/** * @fileoverview Rule to flag the use of redundant constructors in classes. * @author Alberto Rodríguez */ "use strict"; const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ /** * Checks whether any of a method's parameters have a decorator or are a parameter property. * @param {ASTNode} node A method definition node. * @returns {boolean} `true` if any parameter had a decorator or is a parameter property. */ function hasDecoratorsOrParameterProperty(node) { return node.value.params.some( param => param.decorators?.length || param.type === "TSParameterProperty", ); } /** * Checks whether a node's accessibility makes it not useless. * @param {ASTNode} node A method definition node. * @returns {boolean} `true` if the node has a useful accessibility. */ function hasUsefulAccessibility(node) { switch (node.accessibility) { case "protected": case "private": return true; case "public": return !!node.parent.parent.superClass; default: return false; } } /** * Checks whether a given array of statements is a single call of `super`. * @param {ASTNode[]} body An array of statements to check. * @returns {boolean} `true` if the body is a single call of `super`. */ function isSingleSuperCall(body) { return ( body.length === 1 && body[0].type === "ExpressionStatement" && body[0].expression.type === "CallExpression" && body[0].expression.callee.type === "Super" ); } /** * Checks whether a given node is a pattern which doesn't have any side effects. * Default parameters and Destructuring parameters can have side effects. * @param {ASTNode} node A pattern node. * @returns {boolean} `true` if the node doesn't have any side effects. */ function isSimple(node) { return node.type === "Identifier" || node.type === "RestElement"; } /** * Checks whether a given array of expressions is `...arguments` or not. * `super(...arguments)` passes all arguments through. * @param {ASTNode[]} superArgs An array of expressions to check. * @returns {boolean} `true` if the superArgs is `...arguments`. */ function isSpreadArguments(superArgs) { return ( superArgs.length === 1 && superArgs[0].type === "SpreadElement" && superArgs[0].argument.type === "Identifier" && superArgs[0].argument.name === "arguments" ); } /** * Checks whether given 2 nodes are identifiers which have the same name or not. * @param {ASTNode} ctorParam A node to check. * @param {ASTNode} superArg A node to check. * @returns {boolean} `true` if the nodes are identifiers which have the same * name. */ function isValidIdentifierPair(ctorParam, superArg) { return ( ctorParam.type === "Identifier" && superArg.type === "Identifier" && ctorParam.name === superArg.name ); } /** * Checks whether given 2 nodes are a rest/spread pair which has the same values. * @param {ASTNode} ctorParam A node to check. * @param {ASTNode} superArg A node to check. * @returns {boolean} `true` if the nodes are a rest/spread pair which has the * same values. */ function isValidRestSpreadPair(ctorParam, superArg) { return ( ctorParam.type === "RestElement" && superArg.type === "SpreadElement" && isValidIdentifierPair(ctorParam.argument, superArg.argument) ); } /** * Checks whether given 2 nodes have the same value or not. * @param {ASTNode} ctorParam A node to check. * @param {ASTNode} superArg A node to check. * @returns {boolean} `true` if the nodes have the same value or not. */ function isValidPair(ctorParam, superArg) { return ( isValidIdentifierPair(ctorParam, superArg) || isValidRestSpreadPair(ctorParam, superArg) ); } /** * Checks whether the parameters of a constructor and the arguments of `super()` * have the same values or not. * @param {ASTNode} ctorParams The parameters of a constructor to check. * @param {ASTNode} superArgs The arguments of `super()` to check. * @returns {boolean} `true` if those have the same values. */ function isPassingThrough(ctorParams, superArgs) { if (ctorParams.length !== superArgs.length) { return false; } for (let i = 0; i < ctorParams.length; ++i) { if (!isValidPair(ctorParams[i], superArgs[i])) { return false; } } return true; } /** * Checks whether the constructor body is a redundant super call. * @param {Array} body constructor body content. * @param {Array} ctorParams The params to check against super call. * @returns {boolean} true if the constructor body is redundant */ function isRedundantSuperCall(body, ctorParams) { return ( isSingleSuperCall(body) && ctorParams.every(isSimple) && (isSpreadArguments(body[0].expression.arguments) || isPassingThrough(ctorParams, body[0].expression.arguments)) ); } //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { dialects: ["javascript", "typescript"], language: "javascript", type: "suggestion", docs: { description: "Disallow unnecessary constructors", recommended: false, url: "https://eslint.org/docs/latest/rules/no-useless-constructor", }, hasSuggestions: true, schema: [], messages: { noUselessConstructor: "Useless constructor.", removeConstructor: "Remove the constructor.", }, }, create(context) { /** * Checks whether a node is a redundant constructor * @param {ASTNode} node node to check * @returns {void} */ function checkForConstructor(node) { if ( node.kind !== "constructor" || node.value.type !== "FunctionExpression" || hasDecoratorsOrParameterProperty(node) || hasUsefulAccessibility(node) ) { return; } /* * Prevent crashing on parsers which do not require class constructor * to have a body, e.g. typescript and flow */ if (!node.value.body) { return; } const body = node.value.body.body; const ctorParams = node.value.params; const superClass = node.parent.parent.superClass; if ( superClass ? isRedundantSuperCall(body, ctorParams) : body.length === 0 ) { context.report({ node, messageId: "noUselessConstructor", suggest: [ { messageId: "removeConstructor", *fix(fixer) { const nextToken = context.sourceCode.getTokenAfter(node); const addSemiColon = nextToken.type === "Punctuator" && nextToken.value === "[" && astUtils.needsPrecedingSemicolon( context.sourceCode, node, ); yield fixer.replaceText( node, addSemiColon ? ";" : "", ); }, }, ], }); } } return { MethodDefinition: checkForConstructor, }; }, };