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"); 12const astUtils = require("./utils/ast-utils"); 13const keywords = require("./utils/keywords"); 14 15//------------------------------------------------------------------------------ 16// Rule Definition 17//------------------------------------------------------------------------------ 18 19module.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); 157 } catch { 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); 242 } catch { 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}; 308