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