UNPKG

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