選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 

299 行
10 KiB

  1. /**
  2. * @fileoverview A rule to choose between single and double quote marks
  3. * @author Matt DuVall <http://www.mattduvall.com/>, Brandon Payton
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("../util/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Constants
  12. //------------------------------------------------------------------------------
  13. const QUOTE_SETTINGS = {
  14. double: {
  15. quote: "\"",
  16. alternateQuote: "'",
  17. description: "doublequote"
  18. },
  19. single: {
  20. quote: "'",
  21. alternateQuote: "\"",
  22. description: "singlequote"
  23. },
  24. backtick: {
  25. quote: "`",
  26. alternateQuote: "\"",
  27. description: "backtick"
  28. }
  29. };
  30. // An unescaped newline is a newline preceded by an even number of backslashes.
  31. const UNESCAPED_LINEBREAK_PATTERN = new RegExp(String.raw`(^|[^\\])(\\\\)*[${Array.from(astUtils.LINEBREAKS).join("")}]`);
  32. /**
  33. * Switches quoting of javascript string between ' " and `
  34. * escaping and unescaping as necessary.
  35. * Only escaping of the minimal set of characters is changed.
  36. * Note: escaping of newlines when switching from backtick to other quotes is not handled.
  37. * @param {string} str - A string to convert.
  38. * @returns {string} The string with changed quotes.
  39. * @private
  40. */
  41. QUOTE_SETTINGS.double.convert =
  42. QUOTE_SETTINGS.single.convert =
  43. QUOTE_SETTINGS.backtick.convert = function(str) {
  44. const newQuote = this.quote;
  45. const oldQuote = str[0];
  46. if (newQuote === oldQuote) {
  47. return str;
  48. }
  49. return newQuote + str.slice(1, -1).replace(/\\(\${|\r\n?|\n|.)|["'`]|\${|(\r\n?|\n)/g, (match, escaped, newline) => {
  50. if (escaped === oldQuote || oldQuote === "`" && escaped === "${") {
  51. return escaped; // unescape
  52. }
  53. if (match === newQuote || newQuote === "`" && match === "${") {
  54. return `\\${match}`; // escape
  55. }
  56. if (newline && oldQuote === "`") {
  57. return "\\n"; // escape newlines
  58. }
  59. return match;
  60. }) + newQuote;
  61. };
  62. const AVOID_ESCAPE = "avoid-escape";
  63. //------------------------------------------------------------------------------
  64. // Rule Definition
  65. //------------------------------------------------------------------------------
  66. module.exports = {
  67. meta: {
  68. docs: {
  69. description: "enforce the consistent use of either backticks, double, or single quotes",
  70. category: "Stylistic Issues",
  71. recommended: false,
  72. url: "https://eslint.org/docs/rules/quotes"
  73. },
  74. fixable: "code",
  75. schema: [
  76. {
  77. enum: ["single", "double", "backtick"]
  78. },
  79. {
  80. anyOf: [
  81. {
  82. enum: ["avoid-escape"]
  83. },
  84. {
  85. type: "object",
  86. properties: {
  87. avoidEscape: {
  88. type: "boolean"
  89. },
  90. allowTemplateLiterals: {
  91. type: "boolean"
  92. }
  93. },
  94. additionalProperties: false
  95. }
  96. ]
  97. }
  98. ]
  99. },
  100. create(context) {
  101. const quoteOption = context.options[0],
  102. settings = QUOTE_SETTINGS[quoteOption || "double"],
  103. options = context.options[1],
  104. allowTemplateLiterals = options && options.allowTemplateLiterals === true,
  105. sourceCode = context.getSourceCode();
  106. let avoidEscape = options && options.avoidEscape === true;
  107. // deprecated
  108. if (options === AVOID_ESCAPE) {
  109. avoidEscape = true;
  110. }
  111. /**
  112. * Determines if a given node is part of JSX syntax.
  113. *
  114. * This function returns `true` in the following cases:
  115. *
  116. * - `<div className="foo"></div>` ... If the literal is an attribute value, the parent of the literal is `JSXAttribute`.
  117. * - `<div>foo</div>` ... If the literal is a text content, the parent of the literal is `JSXElement`.
  118. * - `<>foo</>` ... If the literal is a text content, the parent of the literal is `JSXFragment`.
  119. *
  120. * In particular, this function returns `false` in the following cases:
  121. *
  122. * - `<div className={"foo"}></div>`
  123. * - `<div>{"foo"}</div>`
  124. *
  125. * In both cases, inside of the braces is handled as normal JavaScript.
  126. * The braces are `JSXExpressionContainer` nodes.
  127. *
  128. * @param {ASTNode} node The Literal node to check.
  129. * @returns {boolean} True if the node is a part of JSX, false if not.
  130. * @private
  131. */
  132. function isJSXLiteral(node) {
  133. return node.parent.type === "JSXAttribute" || node.parent.type === "JSXElement" || node.parent.type === "JSXFragment";
  134. }
  135. /**
  136. * Checks whether or not a given node is a directive.
  137. * The directive is a `ExpressionStatement` which has only a string literal.
  138. * @param {ASTNode} node - A node to check.
  139. * @returns {boolean} Whether or not the node is a directive.
  140. * @private
  141. */
  142. function isDirective(node) {
  143. return (
  144. node.type === "ExpressionStatement" &&
  145. node.expression.type === "Literal" &&
  146. typeof node.expression.value === "string"
  147. );
  148. }
  149. /**
  150. * Checks whether or not a given node is a part of directive prologues.
  151. * See also: http://www.ecma-international.org/ecma-262/6.0/#sec-directive-prologues-and-the-use-strict-directive
  152. * @param {ASTNode} node - A node to check.
  153. * @returns {boolean} Whether or not the node is a part of directive prologues.
  154. * @private
  155. */
  156. function isPartOfDirectivePrologue(node) {
  157. const block = node.parent.parent;
  158. if (block.type !== "Program" && (block.type !== "BlockStatement" || !astUtils.isFunction(block.parent))) {
  159. return false;
  160. }
  161. // Check the node is at a prologue.
  162. for (let i = 0; i < block.body.length; ++i) {
  163. const statement = block.body[i];
  164. if (statement === node.parent) {
  165. return true;
  166. }
  167. if (!isDirective(statement)) {
  168. break;
  169. }
  170. }
  171. return false;
  172. }
  173. /**
  174. * Checks whether or not a given node is allowed as non backtick.
  175. * @param {ASTNode} node - A node to check.
  176. * @returns {boolean} Whether or not the node is allowed as non backtick.
  177. * @private
  178. */
  179. function isAllowedAsNonBacktick(node) {
  180. const parent = node.parent;
  181. switch (parent.type) {
  182. // Directive Prologues.
  183. case "ExpressionStatement":
  184. return isPartOfDirectivePrologue(node);
  185. // LiteralPropertyName.
  186. case "Property":
  187. case "MethodDefinition":
  188. return parent.key === node && !parent.computed;
  189. // ModuleSpecifier.
  190. case "ImportDeclaration":
  191. case "ExportNamedDeclaration":
  192. case "ExportAllDeclaration":
  193. return parent.source === node;
  194. // Others don't allow.
  195. default:
  196. return false;
  197. }
  198. }
  199. return {
  200. Literal(node) {
  201. const val = node.value,
  202. rawVal = node.raw;
  203. if (settings && typeof val === "string") {
  204. let isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) ||
  205. isJSXLiteral(node) ||
  206. astUtils.isSurroundedBy(rawVal, settings.quote);
  207. if (!isValid && avoidEscape) {
  208. isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0;
  209. }
  210. if (!isValid) {
  211. context.report({
  212. node,
  213. message: "Strings must use {{description}}.",
  214. data: {
  215. description: settings.description
  216. },
  217. fix(fixer) {
  218. return fixer.replaceText(node, settings.convert(node.raw));
  219. }
  220. });
  221. }
  222. }
  223. },
  224. TemplateLiteral(node) {
  225. // If backticks are expected or it's a tagged template, then this shouldn't throw an errors
  226. if (
  227. allowTemplateLiterals ||
  228. quoteOption === "backtick" ||
  229. node.parent.type === "TaggedTemplateExpression" && node === node.parent.quasi
  230. ) {
  231. return;
  232. }
  233. // A warning should be produced if the template literal only has one TemplateElement, and has no unescaped newlines.
  234. const shouldWarn = node.quasis.length === 1 && !UNESCAPED_LINEBREAK_PATTERN.test(node.quasis[0].value.raw);
  235. if (shouldWarn) {
  236. context.report({
  237. node,
  238. message: "Strings must use {{description}}.",
  239. data: {
  240. description: settings.description
  241. },
  242. fix(fixer) {
  243. if (isPartOfDirectivePrologue(node)) {
  244. /*
  245. * TemplateLiterals in a directive prologue aren't actually directives, but if they're
  246. * in the directive prologue, then fixing them might turn them into directives and change
  247. * the behavior of the code.
  248. */
  249. return null;
  250. }
  251. return fixer.replaceText(node, settings.convert(sourceCode.getText(node)));
  252. }
  253. });
  254. }
  255. }
  256. };
  257. }
  258. };