no-nonoctal-decimal-escape.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. /**
  2. * @fileoverview Rule to disallow `\8` and `\9` escape sequences in string literals.
  3. * @author Milos Djermanovic
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Helpers
  8. //------------------------------------------------------------------------------
  9. const QUICK_TEST_REGEX = /\\[89]/u;
  10. /**
  11. * Returns unicode escape sequence that represents the given character.
  12. * @param {string} character A single code unit.
  13. * @returns {string} "\uXXXX" sequence.
  14. */
  15. function getUnicodeEscape(character) {
  16. return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
  17. }
  18. //------------------------------------------------------------------------------
  19. // Rule Definition
  20. //------------------------------------------------------------------------------
  21. module.exports = {
  22. meta: {
  23. type: "suggestion",
  24. docs: {
  25. description: "disallow `\\8` and `\\9` escape sequences in string literals",
  26. category: "Best Practices",
  27. recommended: false,
  28. url: "https://eslint.org/docs/rules/no-nonoctal-decimal-escape",
  29. suggestion: true
  30. },
  31. schema: [],
  32. messages: {
  33. decimalEscape: "Don't use '{{decimalEscape}}' escape sequence.",
  34. // suggestions
  35. refactor: "Replace '{{original}}' with '{{replacement}}'. This maintains the current functionality.",
  36. escapeBackslash: "Replace '{{original}}' with '{{replacement}}' to include the actual backslash character."
  37. }
  38. },
  39. create(context) {
  40. const sourceCode = context.getSourceCode();
  41. /**
  42. * Creates a new Suggestion object.
  43. * @param {string} messageId "refactor" or "escapeBackslash".
  44. * @param {int[]} range The range to replace.
  45. * @param {string} replacement New text for the range.
  46. * @returns {Object} Suggestion
  47. */
  48. function createSuggestion(messageId, range, replacement) {
  49. return {
  50. messageId,
  51. data: {
  52. original: sourceCode.getText().slice(...range),
  53. replacement
  54. },
  55. fix(fixer) {
  56. return fixer.replaceTextRange(range, replacement);
  57. }
  58. };
  59. }
  60. return {
  61. Literal(node) {
  62. if (typeof node.value !== "string") {
  63. return;
  64. }
  65. if (!QUICK_TEST_REGEX.test(node.raw)) {
  66. return;
  67. }
  68. const regex = /(?:[^\\]|(?<previousEscape>\\.))*?(?<decimalEscape>\\[89])/suy;
  69. let match;
  70. while ((match = regex.exec(node.raw))) {
  71. const { previousEscape, decimalEscape } = match.groups;
  72. const decimalEscapeRangeEnd = node.range[0] + match.index + match[0].length;
  73. const decimalEscapeRangeStart = decimalEscapeRangeEnd - decimalEscape.length;
  74. const decimalEscapeRange = [decimalEscapeRangeStart, decimalEscapeRangeEnd];
  75. const suggest = [];
  76. // When `regex` is matched, `previousEscape` can only capture characters adjacent to `decimalEscape`
  77. if (previousEscape === "\\0") {
  78. /*
  79. * Now we have a NULL escape "\0" immediately followed by a decimal escape, e.g.: "\0\8".
  80. * Fixing this to "\08" would turn "\0" into a legacy octal escape. To avoid producing
  81. * an octal escape while fixing a decimal escape, we provide different suggestions.
  82. */
  83. suggest.push(
  84. createSuggestion( // "\0\8" -> "\u00008"
  85. "refactor",
  86. [decimalEscapeRangeStart - previousEscape.length, decimalEscapeRangeEnd],
  87. `${getUnicodeEscape("\0")}${decimalEscape[1]}`
  88. ),
  89. createSuggestion( // "\8" -> "\u0038"
  90. "refactor",
  91. decimalEscapeRange,
  92. getUnicodeEscape(decimalEscape[1])
  93. )
  94. );
  95. } else {
  96. suggest.push(
  97. createSuggestion( // "\8" -> "8"
  98. "refactor",
  99. decimalEscapeRange,
  100. decimalEscape[1]
  101. )
  102. );
  103. }
  104. suggest.push(
  105. createSuggestion( // "\8" -> "\\8"
  106. "escapeBackslash",
  107. decimalEscapeRange,
  108. `\\${decimalEscape}`
  109. )
  110. );
  111. context.report({
  112. node,
  113. loc: {
  114. start: sourceCode.getLocFromIndex(decimalEscapeRangeStart),
  115. end: sourceCode.getLocFromIndex(decimalEscapeRangeEnd)
  116. },
  117. messageId: "decimalEscape",
  118. data: {
  119. decimalEscape
  120. },
  121. suggest
  122. });
  123. }
  124. }
  125. };
  126. }
  127. };