Skip to content

Commit

Permalink
New: Add no-nonoctal-decimal-escape rule (fixes #13765) (#13845)
Browse files Browse the repository at this point in the history
* New: Add no-nonoctal-decimal-escape rule (fixes #13765)

* Remove extra blank line

* Add spec link
  • Loading branch information
mdjermanovic committed Nov 20, 2020
1 parent 95d2fe6 commit 98c00c4
Show file tree
Hide file tree
Showing 5 changed files with 699 additions and 0 deletions.
62 changes: 62 additions & 0 deletions docs/rules/no-nonoctal-decimal-escape.md
@@ -0,0 +1,62 @@
# Disallow `\8` and `\9` escape sequences in string literals (no-nonoctal-decimal-escape)

Although not being specified in the language until ECMAScript 2021, `\8` and `\9` escape sequences in string literals were allowed in most JavaScript engines, and treated as "useless" escapes:

```js
"\8" === "8"; // true
"\9" === "9"; // true
```

Since ECMAScript 2021, these escape sequences are specified as [non-octal decimal escape sequences](https://tc39.es/ecma262/#prod-annexB-NonOctalDecimalEscapeSequence), retaining the same behavior.

Nevertheless, the ECMAScript specification treats `\8` and `\9` in string literals as a legacy feature. This syntax is optional if the ECMAScript host is not a web browser. Browsers still have to support it, but only in non-strict mode.

Regardless of your targeted environment, these escape sequences shouldn't be used when writing new code.

## Rule Details

This rule disallows `\8` and `\9` escape sequences in string literals.

Examples of **incorrect** code for this rule:

```js
/*eslint no-nonoctal-decimal-escape: "error"*/

"\8";

"\9";

var foo = "w\8less";

var bar = "December 1\9";

var baz = "Don't use \8 and \9 escapes.";

var quux = "\0\8";
```

Examples of **correct** code for this rule:

```js
/*eslint no-nonoctal-decimal-escape: "error"*/

"8";

"9";

var foo = "w8less";

var bar = "December 19";

var baz = "Don't use \\8 and \\9 escapes.";

var quux = "\0\u0038";
```

## Further Reading

* [NonOctalDecimalEscapeSequence](https://tc39.es/ecma262/#prod-annexB-NonOctalDecimalEscapeSequence) in ECMAScript specification

## Related Rules

* [no-octal-escape](no-octal-escape.md)
1 change: 1 addition & 0 deletions lib/rules/index.js
Expand Up @@ -169,6 +169,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
"no-new-require": () => require("./no-new-require"),
"no-new-symbol": () => require("./no-new-symbol"),
"no-new-wrappers": () => require("./no-new-wrappers"),
"no-nonoctal-decimal-escape": () => require("./no-nonoctal-decimal-escape"),
"no-obj-calls": () => require("./no-obj-calls"),
"no-octal": () => require("./no-octal"),
"no-octal-escape": () => require("./no-octal-escape"),
Expand Down
147 changes: 147 additions & 0 deletions lib/rules/no-nonoctal-decimal-escape.js
@@ -0,0 +1,147 @@
/**
* @fileoverview Rule to disallow `\8` and `\9` escape sequences in string literals.
* @author Milos Djermanovic
*/

"use strict";

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const QUICK_TEST_REGEX = /\\[89]/u;

/**
* Returns unicode escape sequence that represents the given character.
* @param {string} character A single code unit.
* @returns {string} "\uXXXX" sequence.
*/
function getUnicodeEscape(character) {
return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
meta: {
type: "suggestion",

docs: {
description: "disallow `\\8` and `\\9` escape sequences in string literals",
category: "Best Practices",
recommended: false,
url: "https://eslint.org/docs/rules/no-nonoctal-decimal-escape",
suggestion: true
},

schema: [],

messages: {
decimalEscape: "Don't use '{{decimalEscape}}' escape sequence.",

// suggestions
refactor: "Replace '{{original}}' with '{{replacement}}'. This maintains the current functionality.",
escapeBackslash: "Replace '{{original}}' with '{{replacement}}' to include the actual backslash character."
}
},

create(context) {
const sourceCode = context.getSourceCode();

/**
* Creates a new Suggestion object.
* @param {string} messageId "refactor" or "escapeBackslash".
* @param {int[]} range The range to replace.
* @param {string} replacement New text for the range.
* @returns {Object} Suggestion
*/
function createSuggestion(messageId, range, replacement) {
return {
messageId,
data: {
original: sourceCode.getText().slice(...range),
replacement
},
fix(fixer) {
return fixer.replaceTextRange(range, replacement);
}
};
}

return {
Literal(node) {
if (typeof node.value !== "string") {
return;
}

if (!QUICK_TEST_REGEX.test(node.raw)) {
return;
}

const regex = /(?:[^\\]|(?<previousEscape>\\.))*?(?<decimalEscape>\\[89])/suy;
let match;

while ((match = regex.exec(node.raw))) {
const { previousEscape, decimalEscape } = match.groups;
const decimalEscapeRangeEnd = node.range[0] + match.index + match[0].length;
const decimalEscapeRangeStart = decimalEscapeRangeEnd - decimalEscape.length;
const decimalEscapeRange = [decimalEscapeRangeStart, decimalEscapeRangeEnd];
const suggest = [];

// When `regex` is matched, `previousEscape` can only capture characters adjacent to `decimalEscape`
if (previousEscape === "\\0") {

/*
* Now we have a NULL escape "\0" immediately followed by a decimal escape, e.g.: "\0\8".
* Fixing this to "\08" would turn "\0" into a legacy octal escape. To avoid producing
* an octal escape while fixing a decimal escape, we provide different suggestions.
*/
suggest.push(
createSuggestion( // "\0\8" -> "\u00008"
"refactor",
[decimalEscapeRangeStart - previousEscape.length, decimalEscapeRangeEnd],
`${getUnicodeEscape("\0")}${decimalEscape[1]}`
),
createSuggestion( // "\8" -> "\u0038"
"refactor",
decimalEscapeRange,
getUnicodeEscape(decimalEscape[1])
)
);
} else {
suggest.push(
createSuggestion( // "\8" -> "8"
"refactor",
decimalEscapeRange,
decimalEscape[1]
)
);
}

suggest.push(
createSuggestion( // "\8" -> "\\8"
"escapeBackslash",
decimalEscapeRange,
`\\${decimalEscape}`
)
);

context.report({
node,
loc: {
start: sourceCode.getLocFromIndex(decimalEscapeRangeStart),
end: sourceCode.getLocFromIndex(decimalEscapeRangeEnd)
},
messageId: "decimalEscape",
data: {
decimalEscape
},
suggest
});
}
}
};
}
};

0 comments on commit 98c00c4

Please sign in to comment.