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