/** * @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': if (node.parent.type === 'ObjectExpression') { return value !== '__proto__'; } return true; 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('../types').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', defaultOptions: [ { enforceForClassMembers: true, }, ], docs: { description: 'Disallow unnecessary computed property keys in objects and classes', recommended: false, frozen: true, url: 'https://eslint.org/docs/latest/rules/no-useless-computed-key', }, schema: [ { type: 'object', properties: { enforceForClassMembers: { type: 'boolean', }, }, additionalProperties: false, }, ], fixable: 'code', messages: { unnecessarilyComputedProperty: 'Unnecessarily computed property [{{property}}] found.', }, }, create(context) { const sourceCode = context.sourceCode; const [{ enforceForClassMembers }] = context.options; /** * 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, }; }, };