no-nonoctal-decimal-escape.js 4.5 KB

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