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