no-array-constructor.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. /**
  2. * @fileoverview Disallow construction of dense arrays using the Array constructor
  3. * @author Matt DuVall <http://www.mattduvall.com/>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const {
  10. getVariableByName,
  11. isClosingParenToken,
  12. isOpeningParenToken,
  13. isStartOfExpressionStatement,
  14. needsPrecedingSemicolon,
  15. } = require("./utils/ast-utils");
  16. //------------------------------------------------------------------------------
  17. // Rule Definition
  18. //------------------------------------------------------------------------------
  19. /** @type {import('../types').Rule.RuleModule} */
  20. module.exports = {
  21. meta: {
  22. dialects: ["javascript", "typescript"],
  23. language: "javascript",
  24. type: "suggestion",
  25. docs: {
  26. description: "Disallow `Array` constructors",
  27. recommended: false,
  28. url: "https://eslint.org/docs/latest/rules/no-array-constructor",
  29. },
  30. fixable: "code",
  31. hasSuggestions: true,
  32. schema: [],
  33. messages: {
  34. preferLiteral: "The array literal notation [] is preferable.",
  35. useLiteral: "Replace with an array literal.",
  36. useLiteralAfterSemicolon:
  37. "Replace with an array literal, add preceding semicolon.",
  38. },
  39. },
  40. create(context) {
  41. const sourceCode = context.sourceCode;
  42. /**
  43. * Checks if there are comments in Array constructor expressions.
  44. * @param {ASTNode} node A CallExpression or NewExpression node.
  45. * @returns {boolean} True if there are comments, false otherwise.
  46. */
  47. function hasCommentsInArrayConstructor(node) {
  48. const firstToken = sourceCode.getFirstToken(node);
  49. const lastToken = sourceCode.getLastToken(node);
  50. let lastRelevantToken = sourceCode.getLastToken(node.callee);
  51. while (
  52. lastRelevantToken !== lastToken &&
  53. !isOpeningParenToken(lastRelevantToken)
  54. ) {
  55. lastRelevantToken = sourceCode.getTokenAfter(lastRelevantToken);
  56. }
  57. return sourceCode.commentsExistBetween(
  58. firstToken,
  59. lastRelevantToken,
  60. );
  61. }
  62. /**
  63. * Gets the text between the calling parentheses of a CallExpression or NewExpression.
  64. * @param {ASTNode} node A CallExpression or NewExpression node.
  65. * @returns {string} The text between the calling parentheses, or an empty string if there are none.
  66. */
  67. function getArgumentsText(node) {
  68. const lastToken = sourceCode.getLastToken(node);
  69. if (!isClosingParenToken(lastToken)) {
  70. return "";
  71. }
  72. let firstToken = node.callee;
  73. do {
  74. firstToken = sourceCode.getTokenAfter(firstToken);
  75. if (!firstToken || firstToken === lastToken) {
  76. return "";
  77. }
  78. } while (!isOpeningParenToken(firstToken));
  79. return sourceCode.text.slice(
  80. firstToken.range[1],
  81. lastToken.range[0],
  82. );
  83. }
  84. /**
  85. * Disallow construction of dense arrays using the Array constructor
  86. * @param {ASTNode} node node to evaluate
  87. * @returns {void}
  88. * @private
  89. */
  90. function check(node) {
  91. if (
  92. node.callee.type !== "Identifier" ||
  93. node.callee.name !== "Array" ||
  94. node.typeArguments ||
  95. (node.arguments.length === 1 &&
  96. node.arguments[0].type !== "SpreadElement")
  97. ) {
  98. return;
  99. }
  100. const variable = getVariableByName(
  101. sourceCode.getScope(node),
  102. "Array",
  103. );
  104. /*
  105. * Check if `Array` is a predefined global variable: predefined globals have no declarations,
  106. * meaning that the `identifiers` list of the variable object is empty.
  107. */
  108. if (variable && variable.identifiers.length === 0) {
  109. const argsText = getArgumentsText(node);
  110. let fixText;
  111. let messageId;
  112. const nonSpreadCount = node.arguments.reduce(
  113. (count, arg) =>
  114. arg.type !== "SpreadElement" ? count + 1 : count,
  115. 0,
  116. );
  117. const shouldSuggest =
  118. node.optional ||
  119. (node.arguments.length > 0 && nonSpreadCount < 2) ||
  120. hasCommentsInArrayConstructor(node);
  121. /*
  122. * Check if the suggested change should include a preceding semicolon or not.
  123. * Due to JavaScript's ASI rules, a missing semicolon may be inserted automatically
  124. * before an expression like `Array()` or `new Array()`, but not when the expression
  125. * is changed into an array literal like `[]`.
  126. */
  127. if (
  128. isStartOfExpressionStatement(node) &&
  129. needsPrecedingSemicolon(sourceCode, node)
  130. ) {
  131. fixText = `;[${argsText}]`;
  132. messageId = "useLiteralAfterSemicolon";
  133. } else {
  134. fixText = `[${argsText}]`;
  135. messageId = "useLiteral";
  136. }
  137. context.report({
  138. node,
  139. messageId: "preferLiteral",
  140. fix(fixer) {
  141. if (shouldSuggest) {
  142. return null;
  143. }
  144. return fixer.replaceText(node, fixText);
  145. },
  146. suggest: [
  147. {
  148. messageId,
  149. fix(fixer) {
  150. if (shouldSuggest) {
  151. return fixer.replaceText(node, fixText);
  152. }
  153. return null;
  154. },
  155. },
  156. ],
  157. });
  158. }
  159. }
  160. return {
  161. CallExpression: check,
  162. NewExpression: check,
  163. };
  164. },
  165. };