Skip to content

Commit

Permalink
Apply escape-case to regex literal and remove transformation of `\c…
Browse files Browse the repository at this point in the history
…` escape on string literal (#294)

Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
JLHwung and sindresorhus committed Jun 10, 2019
1 parent 3f2e9a6 commit 79748e1
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 18 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -40,6 +40,7 @@
"lodash.snakecase": "^4.0.1",
"lodash.topairs": "^4.3.0",
"lodash.upperfirst": "^4.2.0",
"regexpp": "^2.0.1",
"reserved-words": "^0.1.2",
"safe-regex": "^2.0.1"
},
Expand Down
80 changes: 75 additions & 5 deletions rules/escape-case.js
@@ -1,12 +1,18 @@
'use strict';
const {
visitRegExpAST,
parseRegExpLiteral
} = require('regexpp');

const getDocsUrl = require('./utils/get-docs-url');

const escapeWithLowercase = /((?:^|[^\\])(?:\\\\)*)\\(x[a-f\d]{2}|u[a-f\d]{4}|u\{(?:[a-f\d]{1,})\}|c[a-z])/;
const escapeWithLowercase = /((?:^|[^\\])(?:\\\\)*)\\(x[a-f\d]{2}|u[a-f\d]{4}|u{(?:[a-f\d]+)})/;
const escapePatternWithLowercase = /((?:^|[^\\])(?:\\\\)*)\\(x[a-f\d]{2}|u[a-f\d]{4}|u{(?:[a-f\d]+)}|c[a-z])/;
const hasLowercaseCharacter = /[a-z]+/;
const message = 'Use uppercase characters for the value of the escape sequence.';

const fix = value => {
const results = escapeWithLowercase.exec(value);
const fix = (value, regexp) => {
const results = regexp.exec(value);

if (results) {
const prefix = results[1].length + 1;
Expand All @@ -17,6 +23,56 @@ const fix = value => {
return value;
};

/**
Find the `[start, end]` position of the lowercase escape sequence in a regular expression literal ASTNode.
@param {string} value - String representation of a literal ASTNode.
@returns {number[] | undefined} The `[start, end]` pair if found, or null if not.
*/
const findLowercaseEscape = value => {
const ast = parseRegExpLiteral(value);

let escapeNodePosition;
visitRegExpAST(ast, {
/**
Record escaped node position in regexpp ASTNode. Returns undefined if not found.
@param {ASTNode} node A regexpp ASTNode. Note that it is of different type to the ASTNode of ESLint parsers
@returns {undefined}
*/
onCharacterLeave(node) {
if (escapeNodePosition) {
return;
}

const matches = node.raw.match(escapePatternWithLowercase);

if (matches && matches[2].slice(1).match(hasLowercaseCharacter)) {
escapeNodePosition = [node.start, node.end];
}
}
});

return escapeNodePosition;
};

/**
Produce a fix if there is a lowercase escape sequence in the node.
@param {ASTNode} node - The regular expression literal ASTNode to check.
@returns {string} The fixed `node.raw` string.
*/
const fixRegExp = node => {
const escapeNodePosition = findLowercaseEscape(node.raw);
const {raw} = node;

if (escapeNodePosition) {
const [start, end] = escapeNodePosition;
return raw.slice(0, start) + fix(raw.slice(start, end), escapePatternWithLowercase) + raw.slice(end, raw.length);
}

return raw;
};

const create = context => {
return {
Literal(node) {
Expand All @@ -30,7 +86,18 @@ const create = context => {
context.report({
node,
message,
fix: fixer => fixer.replaceTextRange([node.start, node.end], fix(node.raw))
fix: fixer => fixer.replaceText(node, fix(node.raw, escapeWithLowercase))
});
}
},
'Literal[regex]'(node) {
const escapeNodePosition = findLowercaseEscape(node.raw);

if (escapeNodePosition) {
context.report({
node,
message,
fix: fixer => fixer.replaceText(node, fixRegExp(node))
});
}
},
Expand All @@ -42,10 +109,13 @@ const create = context => {
const matches = node.value.raw.match(escapeWithLowercase);

if (matches && matches[2].slice(1).match(hasLowercaseCharacter)) {
// Move cursor inside the head and tail apostrophe
const start = node.range[0] + 1;
const end = node.range[1] - 1;
context.report({
node,
message,
fix: fixer => fixer.replaceTextRange([node.start, node.end], fix(node.value.raw))
fix: fixer => fixer.replaceTextRange([start, end], fix(node.value.raw, escapeWithLowercase))
});
}
}
Expand Down
57 changes: 44 additions & 13 deletions test/escape-case.js
Expand Up @@ -21,11 +21,9 @@ ruleTester.run('escape-case', rule, {
'const foo = "\\xA9";',
'const foo = "\\uD834";',
'const foo = "\\u{1D306}";',
'const foo = "\\cA";',
'const foo = `\\xA9`;',
'const foo = `\\uD834`;',
'const foo = `\\u{1D306}`;',
'const foo = `\\cA`;',
'const foo = `\\uD834foo`;',
'const foo = `foo\\uD834`;',
'const foo = `foo \\uD834`;',
Expand All @@ -44,7 +42,15 @@ ruleTester.run('escape-case', rule, {
'const foo = `foo\\\\xbar`;',
'const foo = `foo\\\\ubarbaz`;',
'const foo = `foo\\\\\\\\xbar`;',
'const foo = `foo\\\\\\\\ubarbaz`;'
'const foo = `foo\\\\\\\\ubarbaz`;',
'const foo = /\\xA9/',
'const foo = /\\uD834/',
'const foo = /\\u{1D306}/u',
'const foo = /\\cA/',
'const foo = new RegExp("/\\xA9")',
'const foo = new RegExp("/\\uD834/")',
'const foo = new RegExp("/\\u{1D306}/", "u")',
'const foo = new RegExp("/\\cA/")'
],
invalid: [
{
Expand All @@ -62,11 +68,6 @@ ruleTester.run('escape-case', rule, {
errors,
output: 'const foo = "\\u{1D306}";'
},
{
code: 'const foo = "\\ca";',
errors,
output: 'const foo = "\\cA";'
},
{
code: 'const foo = `\\xa9`;',
errors,
Expand All @@ -82,11 +83,6 @@ ruleTester.run('escape-case', rule, {
errors,
output: 'const foo = `\\u{1D306}`;'
},
{
code: 'const foo = `\\ca`;',
errors,
output: 'const foo = `\\cA`;'
},
{
code: 'const foo = `\\ud834foo`;',
errors,
Expand Down Expand Up @@ -151,6 +147,41 @@ ruleTester.run('escape-case', rule, {
code: 'const foo = `foo \\\\\\ud834`;',
errors,
output: 'const foo = `foo \\\\\\uD834`;'
},
{
code: 'const foo = /\\xa9/;',
errors,
output: 'const foo = /\\xA9/;'
},
{
code: 'const foo = /\\ud834/',
errors,
output: 'const foo = /\\uD834/'
},
{
code: 'const foo = /\\u{1d306}/u',
errors,
output: 'const foo = /\\u{1D306}/u'
},
{
code: 'const foo = /\\ca/',
errors,
output: 'const foo = /\\cA/'
},
{
code: 'const foo = new RegExp("/\\xa9")',
errors,
output: 'const foo = new RegExp("/\\xA9")'
},
{
code: 'const foo = new RegExp("/\\ud834/")',
errors,
output: 'const foo = new RegExp("/\\uD834/")'
},
{
code: 'const foo = new RegExp("/\\u{1d306}/", "u")',
errors,
output: 'const foo = new RegExp("/\\u{1D306}/", "u")'
}
]
});

0 comments on commit 79748e1

Please sign in to comment.