quotes.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. /**
  2. * @fileoverview A rule to choose between single and double quote marks
  3. * @author Matt DuVall <http://www.mattduvall.com/>, Brandon Payton
  4. * @deprecated in ESLint v8.53.0
  5. */
  6. "use strict";
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const astUtils = require("./utils/ast-utils");
  11. //------------------------------------------------------------------------------
  12. // Constants
  13. //------------------------------------------------------------------------------
  14. const QUOTE_SETTINGS = {
  15. double: {
  16. quote: '"',
  17. alternateQuote: "'",
  18. description: "doublequote",
  19. },
  20. single: {
  21. quote: "'",
  22. alternateQuote: '"',
  23. description: "singlequote",
  24. },
  25. backtick: {
  26. quote: "`",
  27. alternateQuote: '"',
  28. description: "backtick",
  29. },
  30. };
  31. // An unescaped newline is a newline preceded by an even number of backslashes.
  32. const UNESCAPED_LINEBREAK_PATTERN = new RegExp(
  33. String.raw`(^|[^\\])(\\\\)*[${Array.from(astUtils.LINEBREAKS).join("")}]`,
  34. "u",
  35. );
  36. /**
  37. * Switches quoting of javascript string between ' " and `
  38. * escaping and unescaping as necessary.
  39. * Only escaping of the minimal set of characters is changed.
  40. * Note: escaping of newlines when switching from backtick to other quotes is not handled.
  41. * @param {string} str A string to convert.
  42. * @returns {string} The string with changed quotes.
  43. * @private
  44. */
  45. QUOTE_SETTINGS.double.convert =
  46. QUOTE_SETTINGS.single.convert =
  47. QUOTE_SETTINGS.backtick.convert =
  48. function (str) {
  49. const newQuote = this.quote;
  50. const oldQuote = str[0];
  51. if (newQuote === oldQuote) {
  52. return str;
  53. }
  54. return (
  55. newQuote +
  56. str
  57. .slice(1, -1)
  58. .replace(
  59. /\\(\$\{|\r\n?|\n|.)|["'`]|\$\{|(\r\n?|\n)/gu,
  60. (match, escaped, newline) => {
  61. if (
  62. escaped === oldQuote ||
  63. (oldQuote === "`" && escaped === "${")
  64. ) {
  65. return escaped; // unescape
  66. }
  67. if (
  68. match === newQuote ||
  69. (newQuote === "`" && match === "${")
  70. ) {
  71. return `\\${match}`; // escape
  72. }
  73. if (newline && oldQuote === "`") {
  74. return "\\n"; // escape newlines
  75. }
  76. return match;
  77. },
  78. ) +
  79. newQuote
  80. );
  81. };
  82. const AVOID_ESCAPE = "avoid-escape";
  83. //------------------------------------------------------------------------------
  84. // Rule Definition
  85. //------------------------------------------------------------------------------
  86. /** @type {import('../types').Rule.RuleModule} */
  87. module.exports = {
  88. meta: {
  89. deprecated: {
  90. message: "Formatting rules are being moved out of ESLint core.",
  91. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  92. deprecatedSince: "8.53.0",
  93. availableUntil: "11.0.0",
  94. replacedBy: [
  95. {
  96. message:
  97. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  98. url: "https://eslint.style/guide/migration",
  99. plugin: {
  100. name: "@stylistic/eslint-plugin",
  101. url: "https://eslint.style",
  102. },
  103. rule: {
  104. name: "quotes",
  105. url: "https://eslint.style/rules/quotes",
  106. },
  107. },
  108. ],
  109. },
  110. type: "layout",
  111. docs: {
  112. description:
  113. "Enforce the consistent use of either backticks, double, or single quotes",
  114. recommended: false,
  115. url: "https://eslint.org/docs/latest/rules/quotes",
  116. },
  117. fixable: "code",
  118. schema: [
  119. {
  120. enum: ["single", "double", "backtick"],
  121. },
  122. {
  123. anyOf: [
  124. {
  125. enum: ["avoid-escape"],
  126. },
  127. {
  128. type: "object",
  129. properties: {
  130. avoidEscape: {
  131. type: "boolean",
  132. },
  133. allowTemplateLiterals: {
  134. type: "boolean",
  135. },
  136. },
  137. additionalProperties: false,
  138. },
  139. ],
  140. },
  141. ],
  142. messages: {
  143. wrongQuotes: "Strings must use {{description}}.",
  144. },
  145. },
  146. create(context) {
  147. const quoteOption = context.options[0],
  148. settings = QUOTE_SETTINGS[quoteOption || "double"],
  149. options = context.options[1],
  150. allowTemplateLiterals =
  151. options && options.allowTemplateLiterals === true,
  152. sourceCode = context.sourceCode;
  153. let avoidEscape = options && options.avoidEscape === true;
  154. // deprecated
  155. if (options === AVOID_ESCAPE) {
  156. avoidEscape = true;
  157. }
  158. /**
  159. * Determines if a given node is part of JSX syntax.
  160. *
  161. * This function returns `true` in the following cases:
  162. *
  163. * - `<div className="foo"></div>` ... If the literal is an attribute value, the parent of the literal is `JSXAttribute`.
  164. * - `<div>foo</div>` ... If the literal is a text content, the parent of the literal is `JSXElement`.
  165. * - `<>foo</>` ... If the literal is a text content, the parent of the literal is `JSXFragment`.
  166. *
  167. * In particular, this function returns `false` in the following cases:
  168. *
  169. * - `<div className={"foo"}></div>`
  170. * - `<div>{"foo"}</div>`
  171. *
  172. * In both cases, inside of the braces is handled as normal JavaScript.
  173. * The braces are `JSXExpressionContainer` nodes.
  174. * @param {ASTNode} node The Literal node to check.
  175. * @returns {boolean} True if the node is a part of JSX, false if not.
  176. * @private
  177. */
  178. function isJSXLiteral(node) {
  179. return (
  180. node.parent.type === "JSXAttribute" ||
  181. node.parent.type === "JSXElement" ||
  182. node.parent.type === "JSXFragment"
  183. );
  184. }
  185. /**
  186. * Checks whether or not a given node is a directive.
  187. * The directive is a `ExpressionStatement` which has only a string literal not surrounded by
  188. * parentheses.
  189. * @param {ASTNode} node A node to check.
  190. * @returns {boolean} Whether or not the node is a directive.
  191. * @private
  192. */
  193. function isDirective(node) {
  194. return (
  195. node.type === "ExpressionStatement" &&
  196. node.expression.type === "Literal" &&
  197. typeof node.expression.value === "string" &&
  198. !astUtils.isParenthesised(sourceCode, node.expression)
  199. );
  200. }
  201. /**
  202. * Checks whether a specified node is either part of, or immediately follows a (possibly empty) directive prologue.
  203. * @see {@link http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive}
  204. * @param {ASTNode} node A node to check.
  205. * @returns {boolean} Whether a specified node is either part of, or immediately follows a (possibly empty) directive prologue.
  206. * @private
  207. */
  208. function isExpressionInOrJustAfterDirectivePrologue(node) {
  209. if (!astUtils.isTopLevelExpressionStatement(node.parent)) {
  210. return false;
  211. }
  212. const block = node.parent.parent;
  213. // Check the node is at a prologue.
  214. for (let i = 0; i < block.body.length; ++i) {
  215. const statement = block.body[i];
  216. if (statement === node.parent) {
  217. return true;
  218. }
  219. if (!isDirective(statement)) {
  220. break;
  221. }
  222. }
  223. return false;
  224. }
  225. /**
  226. * Checks whether or not a given node is allowed as non backtick.
  227. * @param {ASTNode} node A node to check.
  228. * @returns {boolean} Whether or not the node is allowed as non backtick.
  229. * @private
  230. */
  231. function isAllowedAsNonBacktick(node) {
  232. const parent = node.parent;
  233. switch (parent.type) {
  234. // Directive Prologues.
  235. case "ExpressionStatement":
  236. return (
  237. !astUtils.isParenthesised(sourceCode, node) &&
  238. isExpressionInOrJustAfterDirectivePrologue(node)
  239. );
  240. // LiteralPropertyName.
  241. case "Property":
  242. case "PropertyDefinition":
  243. case "MethodDefinition":
  244. return parent.key === node && !parent.computed;
  245. // ModuleSpecifier.
  246. case "ImportDeclaration":
  247. case "ExportNamedDeclaration":
  248. return parent.source === node;
  249. // ModuleExportName or ModuleSpecifier.
  250. case "ExportAllDeclaration":
  251. return parent.exported === node || parent.source === node;
  252. // ModuleExportName.
  253. case "ImportSpecifier":
  254. return parent.imported === node;
  255. // ModuleExportName.
  256. case "ExportSpecifier":
  257. return parent.local === node || parent.exported === node;
  258. // Others don't allow.
  259. default:
  260. return false;
  261. }
  262. }
  263. /**
  264. * Checks whether or not a given TemplateLiteral node is actually using any of the special features provided by template literal strings.
  265. * @param {ASTNode} node A TemplateLiteral node to check.
  266. * @returns {boolean} Whether or not the TemplateLiteral node is using any of the special features provided by template literal strings.
  267. * @private
  268. */
  269. function isUsingFeatureOfTemplateLiteral(node) {
  270. const hasTag =
  271. node.parent.type === "TaggedTemplateExpression" &&
  272. node === node.parent.quasi;
  273. if (hasTag) {
  274. return true;
  275. }
  276. const hasStringInterpolation = node.expressions.length > 0;
  277. if (hasStringInterpolation) {
  278. return true;
  279. }
  280. const isMultilineString =
  281. node.quasis.length >= 1 &&
  282. UNESCAPED_LINEBREAK_PATTERN.test(node.quasis[0].value.raw);
  283. if (isMultilineString) {
  284. return true;
  285. }
  286. return false;
  287. }
  288. return {
  289. Literal(node) {
  290. const val = node.value,
  291. rawVal = node.raw;
  292. if (settings && typeof val === "string") {
  293. let isValid =
  294. (quoteOption === "backtick" &&
  295. isAllowedAsNonBacktick(node)) ||
  296. isJSXLiteral(node) ||
  297. astUtils.isSurroundedBy(rawVal, settings.quote);
  298. if (!isValid && avoidEscape) {
  299. isValid =
  300. astUtils.isSurroundedBy(
  301. rawVal,
  302. settings.alternateQuote,
  303. ) && rawVal.includes(settings.quote);
  304. }
  305. if (!isValid) {
  306. context.report({
  307. node,
  308. messageId: "wrongQuotes",
  309. data: {
  310. description: settings.description,
  311. },
  312. fix(fixer) {
  313. if (
  314. quoteOption === "backtick" &&
  315. astUtils.hasOctalOrNonOctalDecimalEscapeSequence(
  316. rawVal,
  317. )
  318. ) {
  319. /*
  320. * An octal or non-octal decimal escape sequence in a template literal would
  321. * produce syntax error, even in non-strict mode.
  322. */
  323. return null;
  324. }
  325. return fixer.replaceText(
  326. node,
  327. settings.convert(node.raw),
  328. );
  329. },
  330. });
  331. }
  332. }
  333. },
  334. TemplateLiteral(node) {
  335. // Don't throw an error if backticks are expected or a template literal feature is in use.
  336. if (
  337. allowTemplateLiterals ||
  338. quoteOption === "backtick" ||
  339. isUsingFeatureOfTemplateLiteral(node)
  340. ) {
  341. return;
  342. }
  343. context.report({
  344. node,
  345. messageId: "wrongQuotes",
  346. data: {
  347. description: settings.description,
  348. },
  349. fix(fixer) {
  350. if (
  351. astUtils.isTopLevelExpressionStatement(
  352. node.parent,
  353. ) &&
  354. !astUtils.isParenthesised(sourceCode, node)
  355. ) {
  356. /*
  357. * TemplateLiterals aren't actually directives, but fixing them might turn
  358. * them into directives and change the behavior of the code.
  359. */
  360. return null;
  361. }
  362. return fixer.replaceText(
  363. node,
  364. settings.convert(sourceCode.getText(node)),
  365. );
  366. },
  367. });
  368. },
  369. };
  370. },
  371. };