Permalink
Cannot retrieve contributors at this time
| /** | |
| * @fileoverview Rule to flag non-quoted property names in object literals. | |
| * @author Mathias Bynens <http://mathiasbynens.be/> | |
| */ | |
| "use strict"; | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
| const espree = require("espree"), | |
| keywords = require("./utils/keywords"); | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
| module.exports = { | |
| meta: { | |
| type: "suggestion", | |
| docs: { | |
| description: "require quotes around object literal property names", | |
| category: "Stylistic Issues", | |
| recommended: false, | |
| url: "https://eslint.org/docs/rules/quote-props" | |
| }, | |
| schema: { | |
| anyOf: [ | |
| { | |
| type: "array", | |
| items: [ | |
| { | |
| enum: ["always", "as-needed", "consistent", "consistent-as-needed"] | |
| } | |
| ], | |
| minItems: 0, | |
| maxItems: 1 | |
| }, | |
| { | |
| type: "array", | |
| items: [ | |
| { | |
| enum: ["always", "as-needed", "consistent", "consistent-as-needed"] | |
| }, | |
| { | |
| type: "object", | |
| properties: { | |
| keywords: { | |
| type: "boolean" | |
| }, | |
| unnecessary: { | |
| type: "boolean" | |
| }, | |
| numbers: { | |
| type: "boolean" | |
| } | |
| }, | |
| additionalProperties: false | |
| } | |
| ], | |
| minItems: 0, | |
| maxItems: 2 | |
| } | |
| ] | |
| }, | |
| fixable: "code" | |
| }, | |
| create(context) { | |
| const MODE = context.options[0], | |
| KEYWORDS = context.options[1] && context.options[1].keywords, | |
| CHECK_UNNECESSARY = !context.options[1] || context.options[1].unnecessary !== false, | |
| NUMBERS = context.options[1] && context.options[1].numbers, | |
| MESSAGE_UNNECESSARY = "Unnecessarily quoted property '{{property}}' found.", | |
| MESSAGE_UNQUOTED = "Unquoted property '{{property}}' found.", | |
| MESSAGE_NUMERIC = "Unquoted number literal '{{property}}' used as key.", | |
| MESSAGE_RESERVED = "Unquoted reserved word '{{property}}' used as key.", | |
| sourceCode = context.getSourceCode(); | |
| /** | |
| * Checks whether a certain string constitutes an ES3 token | |
| * @param {string} tokenStr The string to be checked. | |
| * @returns {boolean} `true` if it is an ES3 token. | |
| */ | |
| function isKeyword(tokenStr) { | |
| return keywords.indexOf(tokenStr) >= 0; | |
| } | |
| /** | |
| * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary) | |
| * @param {string} rawKey The raw key value from the source | |
| * @param {espreeTokens} tokens The espree-tokenized node key | |
| * @param {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked | |
| * @returns {boolean} Whether or not a key has redundant quotes. | |
| * @private | |
| */ | |
| function areQuotesRedundant(rawKey, tokens, skipNumberLiterals) { | |
| return tokens.length === 1 && tokens[0].start === 0 && tokens[0].end === rawKey.length && | |
| (["Identifier", "Keyword", "Null", "Boolean"].indexOf(tokens[0].type) >= 0 || | |
| (tokens[0].type === "Numeric" && !skipNumberLiterals && String(+tokens[0].value) === tokens[0].value)); | |
| } | |
| /** | |
| * Returns a string representation of a property node with quotes removed | |
| * @param {ASTNode} key Key AST Node, which may or may not be quoted | |
| * @returns {string} A replacement string for this property | |
| */ | |
| function getUnquotedKey(key) { | |
| return key.type === "Identifier" ? key.name : key.value; | |
| } | |
| /** | |
| * Returns a string representation of a property node with quotes added | |
| * @param {ASTNode} key Key AST Node, which may or may not be quoted | |
| * @returns {string} A replacement string for this property | |
| */ | |
| function getQuotedKey(key) { | |
| if (key.type === "Literal" && typeof key.value === "string") { | |
| // If the key is already a string literal, don't replace the quotes with double quotes. | |
| return sourceCode.getText(key); | |
| } | |
| // Otherwise, the key is either an identifier or a number literal. | |
| return `"${key.type === "Identifier" ? key.name : key.value}"`; | |
| } | |
| /** | |
| * Ensures that a property's key is quoted only when necessary | |
| * @param {ASTNode} node Property AST node | |
| * @returns {void} | |
| */ | |
| function checkUnnecessaryQuotes(node) { | |
| const key = node.key; | |
| if (node.method || node.computed || node.shorthand) { | |
| return; | |
| } | |
| if (key.type === "Literal" && typeof key.value === "string") { | |
| let tokens; | |
| try { | |
| tokens = espree.tokenize(key.value); | |
| } catch (e) { | |
| return; | |
| } | |
| if (tokens.length !== 1) { | |
| return; | |
| } | |
| const isKeywordToken = isKeyword(tokens[0].value); | |
| if (isKeywordToken && KEYWORDS) { | |
| return; | |
| } | |
| if (CHECK_UNNECESSARY && areQuotesRedundant(key.value, tokens, NUMBERS)) { | |
| context.report({ | |
| node, | |
| message: MESSAGE_UNNECESSARY, | |
| data: { property: key.value }, | |
| fix: fixer => fixer.replaceText(key, getUnquotedKey(key)) | |
| }); | |
| } | |
| } else if (KEYWORDS && key.type === "Identifier" && isKeyword(key.name)) { | |
| context.report({ | |
| node, | |
| message: MESSAGE_RESERVED, | |
| data: { property: key.name }, | |
| fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | |
| }); | |
| } else if (NUMBERS && key.type === "Literal" && typeof key.value === "number") { | |
| context.report({ | |
| node, | |
| message: MESSAGE_NUMERIC, | |
| data: { property: key.value }, | |
| fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | |
| }); | |
| } | |
| } | |
| /** | |
| * Ensures that a property's key is quoted | |
| * @param {ASTNode} node Property AST node | |
| * @returns {void} | |
| */ | |
| function checkOmittedQuotes(node) { | |
| const key = node.key; | |
| if (!node.method && !node.computed && !node.shorthand && !(key.type === "Literal" && typeof key.value === "string")) { | |
| context.report({ | |
| node, | |
| message: MESSAGE_UNQUOTED, | |
| data: { property: key.name || key.value }, | |
| fix: fixer => fixer.replaceText(key, getQuotedKey(key)) | |
| }); | |
| } | |
| } | |
| /** | |
| * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes | |
| * @param {ASTNode} node Property AST node | |
| * @param {boolean} checkQuotesRedundancy Whether to check quotes' redundancy | |
| * @returns {void} | |
| */ | |
| function checkConsistency(node, checkQuotesRedundancy) { | |
| const quotedProps = [], | |
| unquotedProps = []; | |
| let keywordKeyName = null, | |
| necessaryQuotes = false; | |
| node.properties.forEach(property => { | |
| const key = property.key; | |
| if (!key || property.method || property.computed || property.shorthand) { | |
| return; | |
| } | |
| if (key.type === "Literal" && typeof key.value === "string") { | |
| quotedProps.push(property); | |
| if (checkQuotesRedundancy) { | |
| let tokens; | |
| try { | |
| tokens = espree.tokenize(key.value); | |
| } catch (e) { | |
| necessaryQuotes = true; | |
| return; | |
| } | |
| necessaryQuotes = necessaryQuotes || !areQuotesRedundant(key.value, tokens) || KEYWORDS && isKeyword(tokens[0].value); | |
| } | |
| } else if (KEYWORDS && checkQuotesRedundancy && key.type === "Identifier" && isKeyword(key.name)) { | |
| unquotedProps.push(property); | |
| necessaryQuotes = true; | |
| keywordKeyName = key.name; | |
| } else { | |
| unquotedProps.push(property); | |
| } | |
| }); | |
| if (checkQuotesRedundancy && quotedProps.length && !necessaryQuotes) { | |
| quotedProps.forEach(property => { | |
| context.report({ | |
| node: property, | |
| message: "Properties shouldn't be quoted as all quotes are redundant.", | |
| fix: fixer => fixer.replaceText(property.key, getUnquotedKey(property.key)) | |
| }); | |
| }); | |
| } else if (unquotedProps.length && keywordKeyName) { | |
| unquotedProps.forEach(property => { | |
| context.report({ | |
| node: property, | |
| message: "Properties should be quoted as '{{property}}' is a reserved word.", | |
| data: { property: keywordKeyName }, | |
| fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key)) | |
| }); | |
| }); | |
| } else if (quotedProps.length && unquotedProps.length) { | |
| unquotedProps.forEach(property => { | |
| context.report({ | |
| node: property, | |
| message: "Inconsistently quoted property '{{key}}' found.", | |
| data: { key: property.key.name || property.key.value }, | |
| fix: fixer => fixer.replaceText(property.key, getQuotedKey(property.key)) | |
| }); | |
| }); | |
| } | |
| } | |
| return { | |
| Property(node) { | |
| if (MODE === "always" || !MODE) { | |
| checkOmittedQuotes(node); | |
| } | |
| if (MODE === "as-needed") { | |
| checkUnnecessaryQuotes(node); | |
| } | |
| }, | |
| ObjectExpression(node) { | |
| if (MODE === "consistent") { | |
| checkConsistency(node, false); | |
| } | |
| if (MODE === "consistent-as-needed") { | |
| checkConsistency(node, true); | |
| } | |
| } | |
| }; | |
| } | |
| }; |