no-unused-vars.js 23 KB

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