]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule to flag non-quoted property names in object literals. | |
3 | * @author Mathias Bynens <http://mathiasbynens.be/> | |
4 | */ | |
5 | "use strict"; | |
6 | ||
7 | //------------------------------------------------------------------------------ | |
8 | // Requirements | |
9 | //------------------------------------------------------------------------------ | |
10 | ||
11 | const espree = require("espree"); | |
12 | const astUtils = require("./utils/ast-utils"); | |
13 | const keywords = require("./utils/keywords"); | |
14 | ||
15 | //------------------------------------------------------------------------------ | |
16 | // Rule Definition | |
17 | //------------------------------------------------------------------------------ | |
18 | ||
19 | module.exports = { | |
20 | meta: { | |
21 | type: "suggestion", | |
22 | ||
23 | docs: { | |
24 | description: "require quotes around object literal property names", | |
25 | category: "Stylistic Issues", | |
26 | recommended: false, | |
27 | url: "https://eslint.org/docs/rules/quote-props" | |
28 | }, | |
29 | ||
30 | schema: { | |
31 | anyOf: [ | |
32 | { | |
33 | type: "array", | |
34 | items: [ | |
35 | { | |
36 | enum: ["always", "as-needed", "consistent", "consistent-as-needed"] | |
37 | } | |
38 | ], | |
39 | minItems: 0, | |
40 | maxItems: 1 | |
41 | }, | |
42 | { | |
43 | type: "array", | |
44 | items: [ | |
45 | { | |
46 | enum: ["always", "as-needed", "consistent", "consistent-as-needed"] | |
47 | }, | |
48 | { | |
49 | type: "object", | |
50 | properties: { | |
51 | keywords: { | |
52 | type: "boolean" | |
53 | }, | |
54 | unnecessary: { | |
55 | type: "boolean" | |
56 | }, | |
57 | numbers: { | |
58 | type: "boolean" | |
59 | } | |
60 | }, | |
61 | additionalProperties: false | |
62 | } | |
63 | ], | |
64 | minItems: 0, | |
65 | maxItems: 2 | |
66 | } | |
67 | ] | |
68 | }, | |
69 | ||
70 | fixable: "code", | |
71 | messages: { | |
72 | requireQuotesDueToReservedWord: "Properties should be quoted as '{{property}}' is a reserved word.", | |
73 | inconsistentlyQuotedProperty: "Inconsistently quoted property '{{key}}' found.", | |
74 | unnecessarilyQuotedProperty: "Unnecessarily quoted property '{{property}}' found.", | |
75 | unquotedReservedProperty: "Unquoted reserved word '{{property}}' used as key.", | |
76 | unquotedNumericProperty: "Unquoted number literal '{{property}}' used as key.", | |
77 | unquotedPropertyFound: "Unquoted property '{{property}}' found.", | |
78 | redundantQuoting: "Properties shouldn't be quoted as all quotes are redundant." | |
79 | } | |
80 | }, | |
81 | ||
82 | create(context) { | |
83 | ||
84 | const MODE = context.options[0], | |
85 | KEYWORDS = context.options[1] && context.options[1].keywords, | |
86 | CHECK_UNNECESSARY = !context.options[1] || context.options[1].unnecessary !== false, | |
87 | NUMBERS = context.options[1] && context.options[1].numbers, | |
88 | ||
89 | sourceCode = context.getSourceCode(); | |
90 | ||
91 | ||
92 | /** | |
93 | * Checks whether a certain string constitutes an ES3 token | |
94 | * @param {string} tokenStr The string to be checked. | |
95 | * @returns {boolean} `true` if it is an ES3 token. | |
96 | */ | |
97 | function isKeyword(tokenStr) { | |
98 | return keywords.indexOf(tokenStr) >= 0; | |
99 | } | |
100 | ||
101 | /** | |
102 | * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary) | |
103 | * @param {string} rawKey The raw key value from the source | |
104 | * @param {espreeTokens} tokens The espree-tokenized node key | |
105 | * @param {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked | |
106 | * @returns {boolean} Whether or not a key has redundant quotes. | |
107 | * @private | |
108 | */ | |
109 | function areQuotesRedundant(rawKey, tokens, skipNumberLiterals) { | |
110 | return tokens.length === 1 && tokens[0].start === 0 && tokens[0].end === rawKey.length && | |
111 | (["Identifier", "Keyword", "Null", "Boolean"].indexOf(tokens[0].type) >= 0 || | |
112 | (tokens[0].type === "Numeric" && !skipNumberLiterals && String(+tokens[0].value) === tokens[0].value)); | |
113 | } | |
114 | ||
115 | /** | |
116 | * Returns a string representation of a property node with quotes removed | |
117 | * @param {ASTNode} key Key AST Node, which may or may not be quoted | |
118 | * @returns {string} A replacement string for this property | |
119 | */ | |
120 | function getUnquotedKey(key) { | |
121 | return key.type === "Identifier" ? key.name : key.value; | |
122 | } | |
123 | ||
124 | /** | |
125 | * Returns a string representation of a property node with quotes added | |
126 | * @param {ASTNode} key Key AST Node, which may or may not be quoted | |
127 | * @returns {string} A replacement string for this property | |
128 | */ | |
129 | function getQuotedKey(key) { | |
130 | if (key.type === "Literal" && typeof key.value === "string") { | |
131 | ||
132 | // If the key is already a string literal, don't replace the quotes with double quotes. | |
133 | return sourceCode.getText(key); | |
134 | } | |
135 | ||
136 | // Otherwise, the key is either an identifier or a number literal. | |
137 | return `"${key.type === "Identifier" ? key.name : key.value}"`; | |
138 | } | |
139 | ||
140 | /** | |
141 | * Ensures that a property's key is quoted only when necessary | |
142 | * @param {ASTNode} node Property AST node | |
143 | * @returns {void} | |
144 | */ | |
145 | function checkUnnecessaryQuotes(node) { | |
146 | const key = node.key; | |
147 | ||
148 | if (node.method || node.computed || node.shorthand) { | |
149 | return; | |
150 | } | |
151 | ||
152 | if (key.type === "Literal" && typeof key.value === "string") { | |
153 | let tokens; | |
154 | ||
155 | try { | |
156 | tokens = espree.tokenize(key.value); | |
d3726936 | 157 | } catch { |
eb39fafa DC |
158 | return; |
159 | } | |
160 | ||
161 | if (tokens.length !== 1) { | |
162 | return; | |
163 | } | |
164 | ||
165 | const isKeywordToken = isKeyword(tokens[0].value); | |
166 | ||
167 | if (isKeywordToken && KEYWORDS) { | |
168 | return; | |
169 | } | |
170 | ||
171 | if (CHECK_UNNECESSARY && areQuotesRedundant(key.value, tokens, NUMBERS)) { | |
172 | context.report({ | |
173 | node, | |
174 | messageId: "unnecessarilyQuotedProperty", | |
175 | data: { property: key.value }, | |
176 | fix: fixer => fixer.replaceText(key, getUnquotedKey(key)) | |
177 | }); | |
178 | } | |
179 | } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) { | |
180 | context.report({ | |
181 | node, | |
182 | messageId: "unquotedReservedProperty", | |
183 | data: { property: key.name }, | |
184 | fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | |
185 | }); | |
186 | } else if (NUMBERS && key.type === "Literal" && astUtils.isNumericLiteral(key)) { | |
187 | context.report({ | |
188 | node, | |
189 | messageId: "unquotedNumericProperty", | |
190 | data: { property: key.value }, | |
191 | fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | |
192 | }); | |
193 | } | |
194 | } | |
195 | ||
196 | /** | |
197 | * Ensures that a property's key is quoted | |
198 | * @param {ASTNode} node Property AST node | |
199 | * @returns {void} | |
200 | */ | |
201 | function checkOmittedQuotes(node) { | |
202 | const key = node.key; | |
203 | ||
204 | if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) { | |
205 | context.report({ | |
206 | node, | |
207 | messageId: "unquotedPropertyFound", | |
208 | data: { property: key.name || key.value }, | |
209 | fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | |
210 | }); | |
211 | } | |
212 | } | |
213 | ||
214 | /** | |
215 | * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes | |
216 | * @param {ASTNode} node Property AST node | |
217 | * @param {boolean} checkQuotesRedundancy Whether to check quotes' redundancy | |
218 | * @returns {void} | |
219 | */ | |
220 | function checkConsistency(node, checkQuotesRedundancy) { | |
221 | const quotedProps = [], | |
222 | unquotedProps = []; | |
223 | let keywordKeyName = null, | |
224 | necessaryQuotes = false; | |
225 | ||
226 | node.properties.forEach(property => { | |
227 | const key = property.key; | |
228 | ||
229 | if (!key || property.method || property.computed || property.shorthand) { | |
230 | return; | |
231 | } | |
232 | ||
233 | if (key.type === "Literal" && typeof key.value === "string") { | |
234 | ||
235 | quotedProps.push(property); | |
236 | ||
237 | if (checkQuotesRedundancy) { | |
238 | let tokens; | |
239 | ||
240 | try { | |
241 | tokens = espree.tokenize(key.value); | |
d3726936 | 242 | } catch { |
eb39fafa DC |
243 | necessaryQuotes = true; |
244 | return; | |
245 | } | |
246 | ||
247 | necessaryQuotes = necessaryQuotes || !areQuotesRedundant(key.value, tokens) || KEYWORDS && isKeyword(tokens[0].value); | |
248 | } | |
249 | } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) { | |
250 | unquotedProps.push(property); | |
251 | necessaryQuotes = true; | |
252 | keywordKeyName = key.name; | |
253 | } else { | |
254 | unquotedProps.push(property); | |
255 | } | |
256 | }); | |
257 | ||
258 | if (checkQuotesRedundancy && quotedProps.length && !necessaryQuotes) { | |
259 | quotedProps.forEach(property => { | |
260 | context.report({ | |
261 | node: property, | |
262 | messageId: "redundantQuoting", | |
263 | fix: fixer => fixer.replaceText(property.key, getUnquotedKey(property.key)) | |
264 | }); | |
265 | }); | |
266 | } else if (unquotedProps.length && keywordKeyName) { | |
267 | unquotedProps.forEach(property => { | |
268 | context.report({ | |
269 | node: property, | |
270 | messageId: "requireQuotesDueToReservedWord", | |
271 | data: { property: keywordKeyName }, | |
272 | fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key)) | |
273 | }); | |
274 | }); | |
275 | } else if (quotedProps.length && unquotedProps.length) { | |
276 | unquotedProps.forEach(property => { | |
277 | context.report({ | |
278 | node: property, | |
279 | messageId: "inconsistentlyQuotedProperty", | |
280 | data: { key: property.key.name || property.key.value }, | |
281 | fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key)) | |
282 | }); | |
283 | }); | |
284 | } | |
285 | } | |
286 | ||
287 | return { | |
288 | Property(node) { | |
289 | if (MODE === "always" || !MODE) { | |
290 | checkOmittedQuotes(node); | |
291 | } | |
292 | if (MODE === "as-needed") { | |
293 | checkUnnecessaryQuotes(node); | |
294 | } | |
295 | }, | |
296 | ObjectExpression(node) { | |
297 | if (MODE === "consistent") { | |
298 | checkConsistency(node, false); | |
299 | } | |
300 | if (MODE === "consistent-as-needed") { | |
301 | checkConsistency(node, true); | |
302 | } | |
303 | } | |
304 | }; | |
305 | ||
306 | } | |
307 | }; |