/** * @fileoverview Disallow construction of dense arrays using the Array constructor * @author Matt DuVall */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const { getVariableByName, isClosingParenToken, isOpeningParenToken, isStartOfExpressionStatement, needsPrecedingSemicolon, } = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../types').Rule.RuleModule} */ module.exports = { meta: { dialects: ["javascript", "typescript"], language: "javascript", type: "suggestion", docs: { description: "Disallow `Array` constructors", recommended: false, url: "https://eslint.org/docs/latest/rules/no-array-constructor", }, fixable: "code", hasSuggestions: true, schema: [], messages: { preferLiteral: "The array literal notation [] is preferable.", useLiteral: "Replace with an array literal.", useLiteralAfterSemicolon: "Replace with an array literal, add preceding semicolon.", }, }, create(context) { const sourceCode = context.sourceCode; /** * Checks if there are comments in Array constructor expressions. * @param {ASTNode} node A CallExpression or NewExpression node. * @returns {boolean} True if there are comments, false otherwise. */ function hasCommentsInArrayConstructor(node) { const firstToken = sourceCode.getFirstToken(node); const lastToken = sourceCode.getLastToken(node); let lastRelevantToken = sourceCode.getLastToken(node.callee); while ( lastRelevantToken !== lastToken && !isOpeningParenToken(lastRelevantToken) ) { lastRelevantToken = sourceCode.getTokenAfter(lastRelevantToken); } return sourceCode.commentsExistBetween( firstToken, lastRelevantToken, ); } /** * Gets the text between the calling parentheses of a CallExpression or NewExpression. * @param {ASTNode} node A CallExpression or NewExpression node. * @returns {string} The text between the calling parentheses, or an empty string if there are none. */ function getArgumentsText(node) { const lastToken = sourceCode.getLastToken(node); if (!isClosingParenToken(lastToken)) { return ""; } let firstToken = node.callee; do { firstToken = sourceCode.getTokenAfter(firstToken); if (!firstToken || firstToken === lastToken) { return ""; } } while (!isOpeningParenToken(firstToken)); return sourceCode.text.slice( firstToken.range[1], lastToken.range[0], ); } /** * Disallow construction of dense arrays using the Array constructor * @param {ASTNode} node node to evaluate * @returns {void} * @private */ function check(node) { if ( node.callee.type !== "Identifier" || node.callee.name !== "Array" || node.typeArguments || (node.arguments.length === 1 && node.arguments[0].type !== "SpreadElement") ) { return; } const variable = getVariableByName( sourceCode.getScope(node), "Array", ); /* * Check if `Array` is a predefined global variable: predefined globals have no declarations, * meaning that the `identifiers` list of the variable object is empty. */ if (variable && variable.identifiers.length === 0) { const argsText = getArgumentsText(node); let fixText; let messageId; const nonSpreadCount = node.arguments.reduce( (count, arg) => arg.type !== "SpreadElement" ? count + 1 : count, 0, ); const shouldSuggest = node.optional || (node.arguments.length > 0 && nonSpreadCount < 2) || hasCommentsInArrayConstructor(node); /* * Check if the suggested change should include a preceding semicolon or not. * Due to JavaScript's ASI rules, a missing semicolon may be inserted automatically * before an expression like `Array()` or `new Array()`, but not when the expression * is changed into an array literal like `[]`. */ if ( isStartOfExpressionStatement(node) && needsPrecedingSemicolon(sourceCode, node) ) { fixText = `;[${argsText}]`; messageId = "useLiteralAfterSemicolon"; } else { fixText = `[${argsText}]`; messageId = "useLiteral"; } context.report({ node, messageId: "preferLiteral", fix(fixer) { if (shouldSuggest) { return null; } return fixer.replaceText(node, fixText); }, suggest: [ { messageId, fix(fixer) { if (shouldSuggest) { return fixer.replaceText(node, fixText); } return null; }, }, ], }); } } return { CallExpression: check, NewExpression: check, }; }, };