]>
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 | ||
77 | module.exports = { | |
78 | meta: { | |
79 | type: "layout", | |
80 | ||
81 | docs: { | |
82 | description: "enforce the consistent use of either backticks, double, or single quotes", | |
83 | category: "Stylistic Issues", | |
84 | recommended: false, | |
85 | url: "https://eslint.org/docs/rules/quotes" | |
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, | |
126 | sourceCode = context.getSourceCode(); | |
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": | |
219 | case "MethodDefinition": | |
220 | return parent.key === node && !parent.computed; | |
221 | ||
222 | // ModuleSpecifier. | |
223 | case "ImportDeclaration": | |
224 | case "ExportNamedDeclaration": | |
225 | case "ExportAllDeclaration": | |
226 | return parent.source === node; | |
227 | ||
228 | // Others don't allow. | |
229 | default: | |
230 | return false; | |
231 | } | |
232 | } | |
233 | ||
234 | /** | |
235 | * Checks whether or not a given TemplateLiteral node is actually using any of the special features provided by template literal strings. | |
236 | * @param {ASTNode} node A TemplateLiteral node to check. | |
237 | * @returns {boolean} Whether or not the TemplateLiteral node is using any of the special features provided by template literal strings. | |
238 | * @private | |
239 | */ | |
240 | function isUsingFeatureOfTemplateLiteral(node) { | |
241 | const hasTag = node.parent.type === "TaggedTemplateExpression" && node === node.parent.quasi; | |
242 | ||
243 | if (hasTag) { | |
244 | return true; | |
245 | } | |
246 | ||
247 | const hasStringInterpolation = node.expressions.length > 0; | |
248 | ||
249 | if (hasStringInterpolation) { | |
250 | return true; | |
251 | } | |
252 | ||
253 | const isMultilineString = node.quasis.length >= 1 && UNESCAPED_LINEBREAK_PATTERN.test(node.quasis[0].value.raw); | |
254 | ||
255 | if (isMultilineString) { | |
256 | return true; | |
257 | } | |
258 | ||
259 | return false; | |
260 | } | |
261 | ||
262 | return { | |
263 | ||
264 | Literal(node) { | |
265 | const val = node.value, | |
266 | rawVal = node.raw; | |
267 | ||
268 | if (settings && typeof val === "string") { | |
269 | let isValid = (quoteOption === "backtick" && isAllowedAsNonBacktick(node)) || | |
270 | isJSXLiteral(node) || | |
271 | astUtils.isSurroundedBy(rawVal, settings.quote); | |
272 | ||
273 | if (!isValid && avoidEscape) { | |
274 | isValid = astUtils.isSurroundedBy(rawVal, settings.alternateQuote) && rawVal.indexOf(settings.quote) >= 0; | |
275 | } | |
276 | ||
277 | if (!isValid) { | |
278 | context.report({ | |
279 | node, | |
280 | messageId: "wrongQuotes", | |
281 | data: { | |
282 | description: settings.description | |
283 | }, | |
284 | fix(fixer) { | |
6f036462 | 285 | if (quoteOption === "backtick" && astUtils.hasOctalOrNonOctalDecimalEscapeSequence(rawVal)) { |
eb39fafa | 286 | |
6f036462 TL |
287 | /* |
288 | * An octal or non-octal decimal escape sequence in a template literal would | |
289 | * produce syntax error, even in non-strict mode. | |
290 | */ | |
eb39fafa DC |
291 | return null; |
292 | } | |
293 | ||
294 | return fixer.replaceText(node, settings.convert(node.raw)); | |
295 | } | |
296 | }); | |
297 | } | |
298 | } | |
299 | }, | |
300 | ||
301 | TemplateLiteral(node) { | |
302 | ||
303 | // Don't throw an error if backticks are expected or a template literal feature is in use. | |
304 | if ( | |
305 | allowTemplateLiterals || | |
306 | quoteOption === "backtick" || | |
307 | isUsingFeatureOfTemplateLiteral(node) | |
308 | ) { | |
309 | return; | |
310 | } | |
311 | ||
312 | context.report({ | |
313 | node, | |
314 | messageId: "wrongQuotes", | |
315 | data: { | |
316 | description: settings.description | |
317 | }, | |
318 | fix(fixer) { | |
319 | if (isPartOfDirectivePrologue(node)) { | |
320 | ||
321 | /* | |
322 | * TemplateLiterals in a directive prologue aren't actually directives, but if they're | |
323 | * in the directive prologue, then fixing them might turn them into directives and change | |
324 | * the behavior of the code. | |
325 | */ | |
326 | return null; | |
327 | } | |
328 | return fixer.replaceText(node, settings.convert(sourceCode.getText(node))); | |
329 | } | |
330 | }); | |
331 | } | |
332 | }; | |
333 | ||
334 | } | |
335 | }; |