| /** |
| * @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); |
| } |
| } |
| }; |
| |
| } |
| }; |