2 * @fileoverview Rule to flag non-quoted property names in object literals.
3 * @author Mathias Bynens <http://mathiasbynens.be/>
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 const espree
= require("espree");
12 const astUtils
= require("./utils/ast-utils");
13 const keywords
= require("./utils/keywords");
15 //------------------------------------------------------------------------------
17 //------------------------------------------------------------------------------
24 description
: "require quotes around object literal property names",
25 category
: "Stylistic Issues",
27 url
: "https://eslint.org/docs/rules/quote-props"
36 enum: ["always", "as-needed", "consistent", "consistent-as-needed"]
46 enum: ["always", "as-needed", "consistent", "consistent-as-needed"]
61 additionalProperties
: false
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."
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
,
89 sourceCode
= context
.getSourceCode();
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.
97 function isKeyword(tokenStr
) {
98 return keywords
.indexOf(tokenStr
) >= 0;
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.
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
));
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
120 function getUnquotedKey(key
) {
121 return key
.type
=== "Identifier" ? key
.name
: key
.value
;
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
129 function getQuotedKey(key
) {
130 if (key
.type
=== "Literal" && typeof key
.value
=== "string") {
132 // If the key is already a string literal, don't replace the quotes with double quotes.
133 return sourceCode
.getText(key
);
136 // Otherwise, the key is either an identifier or a number literal.
137 return `"${key.type === "Identifier" ? key.name : key.value}"`;
141 * Ensures that a property's key is quoted only when necessary
142 * @param {ASTNode} node Property AST node
145 function checkUnnecessaryQuotes(node
) {
146 const key
= node
.key
;
148 if (node
.method
|| node
.computed
|| node
.shorthand
) {
152 if (key
.type
=== "Literal" && typeof key
.value
=== "string") {
156 tokens
= espree
.tokenize(key
.value
);
161 if (tokens
.length
!== 1) {
165 const isKeywordToken
= isKeyword(tokens
[0].value
);
167 if (isKeywordToken
&& KEYWORDS
) {
171 if (CHECK_UNNECESSARY
&& areQuotesRedundant(key
.value
, tokens
, NUMBERS
)) {
174 messageId
: "unnecessarilyQuotedProperty",
175 data
: { property
: key
.value
},
176 fix
: fixer
=> fixer
.replaceText(key
, getUnquotedKey(key
))
179 } else if (KEYWORDS
&& key
.type
=== "Identifier" && isKeyword(key
.name
)) {
182 messageId
: "unquotedReservedProperty",
183 data
: { property
: key
.name
},
184 fix
: fixer
=> fixer
.replaceText(key
, getQuotedKey(key
))
186 } else if (NUMBERS
&& key
.type
=== "Literal" && astUtils
.isNumericLiteral(key
)) {
189 messageId
: "unquotedNumericProperty",
190 data
: { property
: key
.value
},
191 fix
: fixer
=> fixer
.replaceText(key
, getQuotedKey(key
))
197 * Ensures that a property's key is quoted
198 * @param {ASTNode} node Property AST node
201 function checkOmittedQuotes(node
) {
202 const key
= node
.key
;
204 if (!node
.method
&& !node
.computed
&& !node
.shorthand
&& !(key
.type
=== "Literal" && typeof key
.value
=== "string")) {
207 messageId
: "unquotedPropertyFound",
208 data
: { property
: key
.name
|| key
.value
},
209 fix
: fixer
=> fixer
.replaceText(key
, getQuotedKey(key
))
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
220 function checkConsistency(node
, checkQuotesRedundancy
) {
221 const quotedProps
= [],
223 let keywordKeyName
= null,
224 necessaryQuotes
= false;
226 node
.properties
.forEach(property
=> {
227 const key
= property
.key
;
229 if (!key
|| property
.method
|| property
.computed
|| property
.shorthand
) {
233 if (key
.type
=== "Literal" && typeof key
.value
=== "string") {
235 quotedProps
.push(property
);
237 if (checkQuotesRedundancy
) {
241 tokens
= espree
.tokenize(key
.value
);
243 necessaryQuotes
= true;
247 necessaryQuotes
= necessaryQuotes
|| !areQuotesRedundant(key
.value
, tokens
) || KEYWORDS
&& isKeyword(tokens
[0].value
);
249 } else if (KEYWORDS
&& checkQuotesRedundancy
&& key
.type
=== "Identifier" && isKeyword(key
.name
)) {
250 unquotedProps
.push(property
);
251 necessaryQuotes
= true;
252 keywordKeyName
= key
.name
;
254 unquotedProps
.push(property
);
258 if (checkQuotesRedundancy
&& quotedProps
.length
&& !necessaryQuotes
) {
259 quotedProps
.forEach(property
=> {
262 messageId
: "redundantQuoting",
263 fix
: fixer
=> fixer
.replaceText(property
.key
, getUnquotedKey(property
.key
))
266 } else if (unquotedProps
.length
&& keywordKeyName
) {
267 unquotedProps
.forEach(property
=> {
270 messageId
: "requireQuotesDueToReservedWord",
271 data
: { property
: keywordKeyName
},
272 fix
: fixer
=> fixer
.replaceText(property
.key
, getQuotedKey(property
.key
))
275 } else if (quotedProps
.length
&& unquotedProps
.length
) {
276 unquotedProps
.forEach(property
=> {
279 messageId
: "inconsistentlyQuotedProperty",
280 data
: { key
: property
.key
.name
|| property
.key
.value
},
281 fix
: fixer
=> fixer
.replaceText(property
.key
, getQuotedKey(property
.key
))
289 if (MODE
=== "always" || !MODE
) {
290 checkOmittedQuotes(node
);
292 if (MODE
=== "as-needed") {
293 checkUnnecessaryQuotes(node
);
296 ObjectExpression(node
) {
297 if (MODE
=== "consistent") {
298 checkConsistency(node
, false);
300 if (MODE
=== "consistent-as-needed") {
301 checkConsistency(node
, true);