/** * @fileoverview Rule to disallow unnecessary computed property keys in object literals * @author Burak Yigit Kaya */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Determines whether the computed key syntax is unnecessarily used for the given node. * In particular, it determines whether removing the square brackets and using the content between them * directly as the key (e.g. ['foo'] -> 'foo') would produce valid syntax and preserve the same behavior. * Valid non-computed keys are only: identifiers, number literals and string literals. * Only literals can preserve the same behavior, with a few exceptions for specific node types: * Property * - { ["__proto__"]: foo } defines a property named "__proto__" * { "__proto__": foo } defines object's prototype * PropertyDefinition * - class C { ["constructor"]; } defines an instance field named "constructor" * class C { "constructor"; } produces a parsing error * - class C { static ["constructor"]; } defines a static field named "constructor" * class C { static "constructor"; } produces a parsing error * - class C { static ["prototype"]; } produces a runtime error (doesn't break the whole script) * class C { static "prototype"; } produces a parsing error (breaks the whole script) * MethodDefinition * - class C { ["constructor"]() {} } defines a prototype method named "constructor" * class C { "constructor"() {} } defines the constructor * - class C { static ["prototype"]() {} } produces a runtime error (doesn't break the whole script) * class C { static "prototype"() {} } produces a parsing error (breaks the whole script) * @param {ASTNode} node The node to check. It can be `Property`, `PropertyDefinition` or `MethodDefinition`. * @throws {Error} (Unreachable.) * @returns {void} `true` if the node has useless computed key. */ function hasUselessComputedKey(node) { if (!node.computed) { return false; } const { key } = node; if (key.type !== "Literal") { return false; } const { value } = key; if (typeof value !== "number" && typeof value !== "string") { return false; } switch (node.type) { case "Property": return value !== "__proto__"; case "PropertyDefinition": if (node.static) { return value !== "constructor" && value !== "prototype"; } return value !== "constructor"; case "MethodDefinition": if (node.static) { return value !== "prototype"; } return value !== "constructor"; /* c8 ignore next */ default: throw new Error(`Unexpected node type: ${node.type}`); } } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { type: "suggestion", docs: { description: "Disallow unnecessary computed property keys in objects and classes", recommended: false, url: "https://eslint.org/docs/rules/no-useless-computed-key" }, schema: [{ type: "object", properties: { enforceForClassMembers: { type: "boolean", default: false } }, additionalProperties: false }], fixable: "code", messages: { unnecessarilyComputedProperty: "Unnecessarily computed property [{{property}}] found." } }, create(context) { const sourceCode = context.getSourceCode(); const enforceForClassMembers = context.options[0] && context.options[0].enforceForClassMembers; /** * Reports a given node if it violated this rule. * @param {ASTNode} node The node to check. * @returns {void} */ function check(node) { if (hasUselessComputedKey(node)) { const { key } = node; context.report({ node, messageId: "unnecessarilyComputedProperty", data: { property: sourceCode.getText(key) }, fix(fixer) { const leftSquareBracket = sourceCode.getTokenBefore(key, astUtils.isOpeningBracketToken); const rightSquareBracket = sourceCode.getTokenAfter(key, astUtils.isClosingBracketToken); // If there are comments between the brackets and the property name, don't do a fix. if (sourceCode.commentsExistBetween(leftSquareBracket, rightSquareBracket)) { return null; } const tokenBeforeLeftBracket = sourceCode.getTokenBefore(leftSquareBracket); // Insert a space before the key to avoid changing identifiers, e.g. ({ get[2]() {} }) to ({ get2() {} }) const needsSpaceBeforeKey = tokenBeforeLeftBracket.range[1] === leftSquareBracket.range[0] && !astUtils.canTokensBeAdjacent(tokenBeforeLeftBracket, sourceCode.getFirstToken(key)); const replacementKey = (needsSpaceBeforeKey ? " " : "") + key.raw; return fixer.replaceTextRange([leftSquareBracket.range[0], rightSquareBracket.range[1]], replacementKey); } }); } } /** * A no-op function to act as placeholder for checking a node when the `enforceForClassMembers` option is `false`. * @returns {void} * @private */ function noop() {} return { Property: check, MethodDefinition: enforceForClassMembers ? check : noop, PropertyDefinition: enforceForClassMembers ? check : noop }; } };