no-unused-vars.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. /**
  2. * @fileoverview Rule to flag declared but unused variables
  3. * @author Ilya Volodin
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Typedefs
  12. //------------------------------------------------------------------------------
  13. /**
  14. * Bag of data used for formatting the `unusedVar` lint message.
  15. * @typedef {Object} UnusedVarMessageData
  16. * @property {string} varName The name of the unused var.
  17. * @property {'defined'|'assigned a value'} action Description of the vars state.
  18. * @property {string} additional Any additional info to be appended at the end.
  19. */
  20. //------------------------------------------------------------------------------
  21. // Rule Definition
  22. //------------------------------------------------------------------------------
  23. /** @type {import('../shared/types').Rule} */
  24. module.exports = {
  25. meta: {
  26. type: "problem",
  27. docs: {
  28. description: "Disallow unused variables",
  29. recommended: true,
  30. url: "https://eslint.org/docs/rules/no-unused-vars"
  31. },
  32. schema: [
  33. {
  34. oneOf: [
  35. {
  36. enum: ["all", "local"]
  37. },
  38. {
  39. type: "object",
  40. properties: {
  41. vars: {
  42. enum: ["all", "local"]
  43. },
  44. varsIgnorePattern: {
  45. type: "string"
  46. },
  47. args: {
  48. enum: ["all", "after-used", "none"]
  49. },
  50. ignoreRestSiblings: {
  51. type: "boolean"
  52. },
  53. argsIgnorePattern: {
  54. type: "string"
  55. },
  56. caughtErrors: {
  57. enum: ["all", "none"]
  58. },
  59. caughtErrorsIgnorePattern: {
  60. type: "string"
  61. },
  62. destructuredArrayIgnorePattern: {
  63. type: "string"
  64. }
  65. },
  66. additionalProperties: false
  67. }
  68. ]
  69. }
  70. ],
  71. messages: {
  72. unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}."
  73. }
  74. },
  75. create(context) {
  76. const sourceCode = context.getSourceCode();
  77. const REST_PROPERTY_TYPE = /^(?:RestElement|(?:Experimental)?RestProperty)$/u;
  78. const config = {
  79. vars: "all",
  80. args: "after-used",
  81. ignoreRestSiblings: false,
  82. caughtErrors: "none"
  83. };
  84. const firstOption = context.options[0];
  85. if (firstOption) {
  86. if (typeof firstOption === "string") {
  87. config.vars = firstOption;
  88. } else {
  89. config.vars = firstOption.vars || config.vars;
  90. config.args = firstOption.args || config.args;
  91. config.ignoreRestSiblings = firstOption.ignoreRestSiblings || config.ignoreRestSiblings;
  92. config.caughtErrors = firstOption.caughtErrors || config.caughtErrors;
  93. if (firstOption.varsIgnorePattern) {
  94. config.varsIgnorePattern = new RegExp(firstOption.varsIgnorePattern, "u");
  95. }
  96. if (firstOption.argsIgnorePattern) {
  97. config.argsIgnorePattern = new RegExp(firstOption.argsIgnorePattern, "u");
  98. }
  99. if (firstOption.caughtErrorsIgnorePattern) {
  100. config.caughtErrorsIgnorePattern = new RegExp(firstOption.caughtErrorsIgnorePattern, "u");
  101. }
  102. if (firstOption.destructuredArrayIgnorePattern) {
  103. config.destructuredArrayIgnorePattern = new RegExp(firstOption.destructuredArrayIgnorePattern, "u");
  104. }
  105. }
  106. }
  107. /**
  108. * Generates the message data about the variable being defined and unused,
  109. * including the ignore pattern if configured.
  110. * @param {Variable} unusedVar eslint-scope variable object.
  111. * @returns {UnusedVarMessageData} The message data to be used with this unused variable.
  112. */
  113. function getDefinedMessageData(unusedVar) {
  114. const defType = unusedVar.defs && unusedVar.defs[0] && unusedVar.defs[0].type;
  115. let type;
  116. let pattern;
  117. if (defType === "CatchClause" && config.caughtErrorsIgnorePattern) {
  118. type = "args";
  119. pattern = config.caughtErrorsIgnorePattern.toString();
  120. } else if (defType === "Parameter" && config.argsIgnorePattern) {
  121. type = "args";
  122. pattern = config.argsIgnorePattern.toString();
  123. } else if (defType !== "Parameter" && config.varsIgnorePattern) {
  124. type = "vars";
  125. pattern = config.varsIgnorePattern.toString();
  126. }
  127. const additional = type ? `. Allowed unused ${type} must match ${pattern}` : "";
  128. return {
  129. varName: unusedVar.name,
  130. action: "defined",
  131. additional
  132. };
  133. }
  134. /**
  135. * Generate the warning message about the variable being
  136. * assigned and unused, including the ignore pattern if configured.
  137. * @param {Variable} unusedVar eslint-scope variable object.
  138. * @returns {UnusedVarMessageData} The message data to be used with this unused variable.
  139. */
  140. function getAssignedMessageData(unusedVar) {
  141. const def = unusedVar.defs[0];
  142. let additional = "";
  143. if (config.destructuredArrayIgnorePattern && def && def.name.parent.type === "ArrayPattern") {
  144. additional = `. Allowed unused elements of array destructuring patterns must match ${config.destructuredArrayIgnorePattern.toString()}`;
  145. } else if (config.varsIgnorePattern) {
  146. additional = `. Allowed unused vars must match ${config.varsIgnorePattern.toString()}`;
  147. }
  148. return {
  149. varName: unusedVar.name,
  150. action: "assigned a value",
  151. additional
  152. };
  153. }
  154. //--------------------------------------------------------------------------
  155. // Helpers
  156. //--------------------------------------------------------------------------
  157. const STATEMENT_TYPE = /(?:Statement|Declaration)$/u;
  158. /**
  159. * Determines if a given variable is being exported from a module.
  160. * @param {Variable} variable eslint-scope variable object.
  161. * @returns {boolean} True if the variable is exported, false if not.
  162. * @private
  163. */
  164. function isExported(variable) {
  165. const definition = variable.defs[0];
  166. if (definition) {
  167. let node = definition.node;
  168. if (node.type === "VariableDeclarator") {
  169. node = node.parent;
  170. } else if (definition.type === "Parameter") {
  171. return false;
  172. }
  173. return node.parent.type.indexOf("Export") === 0;
  174. }
  175. return false;
  176. }
  177. /**
  178. * Checks whether a node is a sibling of the rest property or not.
  179. * @param {ASTNode} node a node to check
  180. * @returns {boolean} True if the node is a sibling of the rest property, otherwise false.
  181. */
  182. function hasRestSibling(node) {
  183. return node.type === "Property" &&
  184. node.parent.type === "ObjectPattern" &&
  185. REST_PROPERTY_TYPE.test(node.parent.properties[node.parent.properties.length - 1].type);
  186. }
  187. /**
  188. * Determines if a variable has a sibling rest property
  189. * @param {Variable} variable eslint-scope variable object.
  190. * @returns {boolean} True if the variable is exported, false if not.
  191. * @private
  192. */
  193. function hasRestSpreadSibling(variable) {
  194. if (config.ignoreRestSiblings) {
  195. const hasRestSiblingDefinition = variable.defs.some(def => hasRestSibling(def.name.parent));
  196. const hasRestSiblingReference = variable.references.some(ref => hasRestSibling(ref.identifier.parent));
  197. return hasRestSiblingDefinition || hasRestSiblingReference;
  198. }
  199. return false;
  200. }
  201. /**
  202. * Determines if a reference is a read operation.
  203. * @param {Reference} ref An eslint-scope Reference
  204. * @returns {boolean} whether the given reference represents a read operation
  205. * @private
  206. */
  207. function isReadRef(ref) {
  208. return ref.isRead();
  209. }
  210. /**
  211. * Determine if an identifier is referencing an enclosing function name.
  212. * @param {Reference} ref The reference to check.
  213. * @param {ASTNode[]} nodes The candidate function nodes.
  214. * @returns {boolean} True if it's a self-reference, false if not.
  215. * @private
  216. */
  217. function isSelfReference(ref, nodes) {
  218. let scope = ref.from;
  219. while (scope) {
  220. if (nodes.includes(scope.block)) {
  221. return true;
  222. }
  223. scope = scope.upper;
  224. }
  225. return false;
  226. }
  227. /**
  228. * Gets a list of function definitions for a specified variable.
  229. * @param {Variable} variable eslint-scope variable object.
  230. * @returns {ASTNode[]} Function nodes.
  231. * @private
  232. */
  233. function getFunctionDefinitions(variable) {
  234. const functionDefinitions = [];
  235. variable.defs.forEach(def => {
  236. const { type, node } = def;
  237. // FunctionDeclarations
  238. if (type === "FunctionName") {
  239. functionDefinitions.push(node);
  240. }
  241. // FunctionExpressions
  242. if (type === "Variable" && node.init &&
  243. (node.init.type === "FunctionExpression" || node.init.type === "ArrowFunctionExpression")) {
  244. functionDefinitions.push(node.init);
  245. }
  246. });
  247. return functionDefinitions;
  248. }
  249. /**
  250. * Checks the position of given nodes.
  251. * @param {ASTNode} inner A node which is expected as inside.
  252. * @param {ASTNode} outer A node which is expected as outside.
  253. * @returns {boolean} `true` if the `inner` node exists in the `outer` node.
  254. * @private
  255. */
  256. function isInside(inner, outer) {
  257. return (
  258. inner.range[0] >= outer.range[0] &&
  259. inner.range[1] <= outer.range[1]
  260. );
  261. }
  262. /**
  263. * Checks whether a given node is unused expression or not.
  264. * @param {ASTNode} node The node itself
  265. * @returns {boolean} The node is an unused expression.
  266. * @private
  267. */
  268. function isUnusedExpression(node) {
  269. const parent = node.parent;
  270. if (parent.type === "ExpressionStatement") {
  271. return true;
  272. }
  273. if (parent.type === "SequenceExpression") {
  274. const isLastExpression = parent.expressions[parent.expressions.length - 1] === node;
  275. if (!isLastExpression) {
  276. return true;
  277. }
  278. return isUnusedExpression(parent);
  279. }
  280. return false;
  281. }
  282. /**
  283. * If a given reference is left-hand side of an assignment, this gets
  284. * the right-hand side node of the assignment.
  285. *
  286. * In the following cases, this returns null.
  287. *
  288. * - The reference is not the LHS of an assignment expression.
  289. * - The reference is inside of a loop.
  290. * - The reference is inside of a function scope which is different from
  291. * the declaration.
  292. * @param {eslint-scope.Reference} ref A reference to check.
  293. * @param {ASTNode} prevRhsNode The previous RHS node.
  294. * @returns {ASTNode|null} The RHS node or null.
  295. * @private
  296. */
  297. function getRhsNode(ref, prevRhsNode) {
  298. const id = ref.identifier;
  299. const parent = id.parent;
  300. const refScope = ref.from.variableScope;
  301. const varScope = ref.resolved.scope.variableScope;
  302. const canBeUsedLater = refScope !== varScope || astUtils.isInLoop(id);
  303. /*
  304. * Inherits the previous node if this reference is in the node.
  305. * This is for `a = a + a`-like code.
  306. */
  307. if (prevRhsNode && isInside(id, prevRhsNode)) {
  308. return prevRhsNode;
  309. }
  310. if (parent.type === "AssignmentExpression" &&
  311. isUnusedExpression(parent) &&
  312. id === parent.left &&
  313. !canBeUsedLater
  314. ) {
  315. return parent.right;
  316. }
  317. return null;
  318. }
  319. /**
  320. * Checks whether a given function node is stored to somewhere or not.
  321. * If the function node is stored, the function can be used later.
  322. * @param {ASTNode} funcNode A function node to check.
  323. * @param {ASTNode} rhsNode The RHS node of the previous assignment.
  324. * @returns {boolean} `true` if under the following conditions:
  325. * - the funcNode is assigned to a variable.
  326. * - the funcNode is bound as an argument of a function call.
  327. * - the function is bound to a property and the object satisfies above conditions.
  328. * @private
  329. */
  330. function isStorableFunction(funcNode, rhsNode) {
  331. let node = funcNode;
  332. let parent = funcNode.parent;
  333. while (parent && isInside(parent, rhsNode)) {
  334. switch (parent.type) {
  335. case "SequenceExpression":
  336. if (parent.expressions[parent.expressions.length - 1] !== node) {
  337. return false;
  338. }
  339. break;
  340. case "CallExpression":
  341. case "NewExpression":
  342. return parent.callee !== node;
  343. case "AssignmentExpression":
  344. case "TaggedTemplateExpression":
  345. case "YieldExpression":
  346. return true;
  347. default:
  348. if (STATEMENT_TYPE.test(parent.type)) {
  349. /*
  350. * If it encountered statements, this is a complex pattern.
  351. * Since analyzing complex patterns is hard, this returns `true` to avoid false positive.
  352. */
  353. return true;
  354. }
  355. }
  356. node = parent;
  357. parent = parent.parent;
  358. }
  359. return false;
  360. }
  361. /**
  362. * Checks whether a given Identifier node exists inside of a function node which can be used later.
  363. *
  364. * "can be used later" means:
  365. * - the function is assigned to a variable.
  366. * - the function is bound to a property and the object can be used later.
  367. * - the function is bound as an argument of a function call.
  368. *
  369. * If a reference exists in a function which can be used later, the reference is read when the function is called.
  370. * @param {ASTNode} id An Identifier node to check.
  371. * @param {ASTNode} rhsNode The RHS node of the previous assignment.
  372. * @returns {boolean} `true` if the `id` node exists inside of a function node which can be used later.
  373. * @private
  374. */
  375. function isInsideOfStorableFunction(id, rhsNode) {
  376. const funcNode = astUtils.getUpperFunction(id);
  377. return (
  378. funcNode &&
  379. isInside(funcNode, rhsNode) &&
  380. isStorableFunction(funcNode, rhsNode)
  381. );
  382. }
  383. /**
  384. * Checks whether a given reference is a read to update itself or not.
  385. * @param {eslint-scope.Reference} ref A reference to check.
  386. * @param {ASTNode} rhsNode The RHS node of the previous assignment.
  387. * @returns {boolean} The reference is a read to update itself.
  388. * @private
  389. */
  390. function isReadForItself(ref, rhsNode) {
  391. const id = ref.identifier;
  392. const parent = id.parent;
  393. return ref.isRead() && (
  394. // self update. e.g. `a += 1`, `a++`
  395. (
  396. (
  397. parent.type === "AssignmentExpression" &&
  398. parent.left === id &&
  399. isUnusedExpression(parent)
  400. ) ||
  401. (
  402. parent.type === "UpdateExpression" &&
  403. isUnusedExpression(parent)
  404. )
  405. ) ||
  406. // in RHS of an assignment for itself. e.g. `a = a + 1`
  407. (
  408. rhsNode &&
  409. isInside(id, rhsNode) &&
  410. !isInsideOfStorableFunction(id, rhsNode)
  411. )
  412. );
  413. }
  414. /**
  415. * Determine if an identifier is used either in for-in or for-of loops.
  416. * @param {Reference} ref The reference to check.
  417. * @returns {boolean} whether reference is used in the for-in loops
  418. * @private
  419. */
  420. function isForInOfRef(ref) {
  421. let target = ref.identifier.parent;
  422. // "for (var ...) { return; }"
  423. if (target.type === "VariableDeclarator") {
  424. target = target.parent.parent;
  425. }
  426. if (target.type !== "ForInStatement" && target.type !== "ForOfStatement") {
  427. return false;
  428. }
  429. // "for (...) { return; }"
  430. if (target.body.type === "BlockStatement") {
  431. target = target.body.body[0];
  432. // "for (...) return;"
  433. } else {
  434. target = target.body;
  435. }
  436. // For empty loop body
  437. if (!target) {
  438. return false;
  439. }
  440. return target.type === "ReturnStatement";
  441. }
  442. /**
  443. * Determines if the variable is used.
  444. * @param {Variable} variable The variable to check.
  445. * @returns {boolean} True if the variable is used
  446. * @private
  447. */
  448. function isUsedVariable(variable) {
  449. const functionNodes = getFunctionDefinitions(variable),
  450. isFunctionDefinition = functionNodes.length > 0;
  451. let rhsNode = null;
  452. return variable.references.some(ref => {
  453. if (isForInOfRef(ref)) {
  454. return true;
  455. }
  456. const forItself = isReadForItself(ref, rhsNode);
  457. rhsNode = getRhsNode(ref, rhsNode);
  458. return (
  459. isReadRef(ref) &&
  460. !forItself &&
  461. !(isFunctionDefinition && isSelfReference(ref, functionNodes))
  462. );
  463. });
  464. }
  465. /**
  466. * Checks whether the given variable is after the last used parameter.
  467. * @param {eslint-scope.Variable} variable The variable to check.
  468. * @returns {boolean} `true` if the variable is defined after the last
  469. * used parameter.
  470. */
  471. function isAfterLastUsedArg(variable) {
  472. const def = variable.defs[0];
  473. const params = context.getDeclaredVariables(def.node);
  474. const posteriorParams = params.slice(params.indexOf(variable) + 1);
  475. // If any used parameters occur after this parameter, do not report.
  476. return !posteriorParams.some(v => v.references.length > 0 || v.eslintUsed);
  477. }
  478. /**
  479. * Gets an array of variables without read references.
  480. * @param {Scope} scope an eslint-scope Scope object.
  481. * @param {Variable[]} unusedVars an array that saving result.
  482. * @returns {Variable[]} unused variables of the scope and descendant scopes.
  483. * @private
  484. */
  485. function collectUnusedVariables(scope, unusedVars) {
  486. const variables = scope.variables;
  487. const childScopes = scope.childScopes;
  488. let i, l;
  489. if (scope.type !== "global" || config.vars === "all") {
  490. for (i = 0, l = variables.length; i < l; ++i) {
  491. const variable = variables[i];
  492. // skip a variable of class itself name in the class scope
  493. if (scope.type === "class" && scope.block.id === variable.identifiers[0]) {
  494. continue;
  495. }
  496. // skip function expression names and variables marked with markVariableAsUsed()
  497. if (scope.functionExpressionScope || variable.eslintUsed) {
  498. continue;
  499. }
  500. // skip implicit "arguments" variable
  501. if (scope.type === "function" && variable.name === "arguments" && variable.identifiers.length === 0) {
  502. continue;
  503. }
  504. // explicit global variables don't have definitions.
  505. const def = variable.defs[0];
  506. if (def) {
  507. const type = def.type;
  508. const refUsedInArrayPatterns = variable.references.some(ref => ref.identifier.parent.type === "ArrayPattern");
  509. // skip elements of array destructuring patterns
  510. if (
  511. (
  512. def.name.parent.type === "ArrayPattern" ||
  513. refUsedInArrayPatterns
  514. ) &&
  515. config.destructuredArrayIgnorePattern &&
  516. config.destructuredArrayIgnorePattern.test(def.name.name)
  517. ) {
  518. continue;
  519. }
  520. // skip catch variables
  521. if (type === "CatchClause") {
  522. if (config.caughtErrors === "none") {
  523. continue;
  524. }
  525. // skip ignored parameters
  526. if (config.caughtErrorsIgnorePattern && config.caughtErrorsIgnorePattern.test(def.name.name)) {
  527. continue;
  528. }
  529. }
  530. if (type === "Parameter") {
  531. // skip any setter argument
  532. if ((def.node.parent.type === "Property" || def.node.parent.type === "MethodDefinition") && def.node.parent.kind === "set") {
  533. continue;
  534. }
  535. // if "args" option is "none", skip any parameter
  536. if (config.args === "none") {
  537. continue;
  538. }
  539. // skip ignored parameters
  540. if (config.argsIgnorePattern && config.argsIgnorePattern.test(def.name.name)) {
  541. continue;
  542. }
  543. // if "args" option is "after-used", skip used variables
  544. if (config.args === "after-used" && astUtils.isFunction(def.name.parent) && !isAfterLastUsedArg(variable)) {
  545. continue;
  546. }
  547. } else {
  548. // skip ignored variables
  549. if (config.varsIgnorePattern && config.varsIgnorePattern.test(def.name.name)) {
  550. continue;
  551. }
  552. }
  553. }
  554. if (!isUsedVariable(variable) && !isExported(variable) && !hasRestSpreadSibling(variable)) {
  555. unusedVars.push(variable);
  556. }
  557. }
  558. }
  559. for (i = 0, l = childScopes.length; i < l; ++i) {
  560. collectUnusedVariables(childScopes[i], unusedVars);
  561. }
  562. return unusedVars;
  563. }
  564. //--------------------------------------------------------------------------
  565. // Public
  566. //--------------------------------------------------------------------------
  567. return {
  568. "Program:exit"(programNode) {
  569. const unusedVars = collectUnusedVariables(context.getScope(), []);
  570. for (let i = 0, l = unusedVars.length; i < l; ++i) {
  571. const unusedVar = unusedVars[i];
  572. // Report the first declaration.
  573. if (unusedVar.defs.length > 0) {
  574. // report last write reference, https://github.com/eslint/eslint/issues/14324
  575. const writeReferences = unusedVar.references.filter(ref => ref.isWrite() && ref.from.variableScope === unusedVar.scope.variableScope);
  576. let referenceToReport;
  577. if (writeReferences.length > 0) {
  578. referenceToReport = writeReferences[writeReferences.length - 1];
  579. }
  580. context.report({
  581. node: referenceToReport ? referenceToReport.identifier : unusedVar.identifiers[0],
  582. messageId: "unusedVar",
  583. data: unusedVar.references.some(ref => ref.isWrite())
  584. ? getAssignedMessageData(unusedVar)
  585. : getDefinedMessageData(unusedVar)
  586. });
  587. // If there are no regular declaration, report the first `/*globals*/` comment directive.
  588. } else if (unusedVar.eslintExplicitGlobalComments) {
  589. const directiveComment = unusedVar.eslintExplicitGlobalComments[0];
  590. context.report({
  591. node: programNode,
  592. loc: astUtils.getNameLocationInGlobalDirectiveComment(sourceCode, directiveComment, unusedVar.name),
  593. messageId: "unusedVar",
  594. data: getDefinedMessageData(unusedVar)
  595. });
  596. }
  597. }
  598. }
  599. };
  600. }
  601. };