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);