no-useless-undefined.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. 'use strict';
  2. const {isCommaToken} = require('eslint-utils');
  3. const {replaceNodeOrTokenAndSpacesBefore} = require('./fix/index.js');
  4. const messageId = 'no-useless-undefined';
  5. const messages = {
  6. [messageId]: 'Do not use useless `undefined`.',
  7. };
  8. const getSelector = (parent, property) =>
  9. `${parent} > Identifier.${property}[name="undefined"]`;
  10. // `return undefined`
  11. const returnSelector = getSelector('ReturnStatement', 'argument');
  12. // `yield undefined`
  13. const yieldSelector = getSelector('YieldExpression[delegate!=true]', 'argument');
  14. // `() => undefined`
  15. const arrowFunctionSelector = getSelector('ArrowFunctionExpression', 'body');
  16. // `let foo = undefined` / `var foo = undefined`
  17. const variableInitSelector = getSelector(
  18. [
  19. 'VariableDeclaration',
  20. '[kind!="const"]',
  21. '>',
  22. 'VariableDeclarator',
  23. ].join(''),
  24. 'init',
  25. );
  26. // `const {foo = undefined} = {}`
  27. const assignmentPatternSelector = getSelector('AssignmentPattern', 'right');
  28. const isUndefined = node => node && node.type === 'Identifier' && node.name === 'undefined';
  29. const compareFunctionNames = new Set([
  30. 'is',
  31. 'equal',
  32. 'notEqual',
  33. 'strictEqual',
  34. 'notStrictEqual',
  35. 'propertyVal',
  36. 'notPropertyVal',
  37. 'not',
  38. 'include',
  39. 'property',
  40. 'toBe',
  41. 'toHaveBeenCalledWith',
  42. 'toContain',
  43. 'toContainEqual',
  44. 'toEqual',
  45. 'same',
  46. 'notSame',
  47. 'strictSame',
  48. 'strictNotSame',
  49. ]);
  50. const shouldIgnore = node => {
  51. let name;
  52. if (node.type === 'Identifier') {
  53. name = node.name;
  54. } else if (
  55. node.type === 'MemberExpression'
  56. && node.computed === false
  57. && node.property
  58. && node.property.type === 'Identifier'
  59. ) {
  60. name = node.property.name;
  61. }
  62. return compareFunctionNames.has(name)
  63. // `set.add(undefined)`
  64. || name === 'add'
  65. // `map.set(foo, undefined)`
  66. || name === 'set'
  67. // `array.push(undefined)`
  68. || name === 'push'
  69. // `array.unshift(undefined)`
  70. || name === 'unshift';
  71. };
  72. const getFunction = scope => {
  73. for (; scope; scope = scope.upper) {
  74. if (scope.type === 'function') {
  75. return scope.block;
  76. }
  77. }
  78. };
  79. /** @param {import('eslint').Rule.RuleContext} context */
  80. const create = context => {
  81. const listener = (fix, checkFunctionReturnType) => node => {
  82. if (checkFunctionReturnType) {
  83. const functionNode = getFunction(context.getScope());
  84. if (functionNode && functionNode.returnType) {
  85. return;
  86. }
  87. }
  88. return {
  89. node,
  90. messageId,
  91. fix: fixer => fix(node, fixer),
  92. };
  93. };
  94. const sourceCode = context.getSourceCode();
  95. const options = {
  96. checkArguments: true,
  97. ...context.options[0],
  98. };
  99. const removeNodeAndLeadingSpace = (node, fixer) =>
  100. replaceNodeOrTokenAndSpacesBefore(node, '', fixer, sourceCode);
  101. const listeners = {
  102. [returnSelector]: listener(
  103. removeNodeAndLeadingSpace,
  104. /* CheckFunctionReturnType */ true,
  105. ),
  106. [yieldSelector]: listener(removeNodeAndLeadingSpace),
  107. [arrowFunctionSelector]: listener(
  108. (node, fixer) => replaceNodeOrTokenAndSpacesBefore(node, ' {}', fixer, sourceCode),
  109. /* CheckFunctionReturnType */ true,
  110. ),
  111. [variableInitSelector]: listener(
  112. (node, fixer) => fixer.removeRange([node.parent.id.range[1], node.range[1]]),
  113. ),
  114. [assignmentPatternSelector]: listener(
  115. (node, fixer) => fixer.removeRange([node.parent.left.range[1], node.range[1]]),
  116. ),
  117. };
  118. if (options.checkArguments) {
  119. listeners.CallExpression = node => {
  120. if (shouldIgnore(node.callee)) {
  121. return;
  122. }
  123. const argumentNodes = node.arguments;
  124. const undefinedArguments = [];
  125. for (let index = argumentNodes.length - 1; index >= 0; index--) {
  126. const node = argumentNodes[index];
  127. if (isUndefined(node)) {
  128. undefinedArguments.unshift(node);
  129. } else {
  130. break;
  131. }
  132. }
  133. if (undefinedArguments.length === 0) {
  134. return;
  135. }
  136. const firstUndefined = undefinedArguments[0];
  137. const lastUndefined = undefinedArguments[undefinedArguments.length - 1];
  138. return {
  139. messageId,
  140. loc: {
  141. start: firstUndefined.loc.start,
  142. end: lastUndefined.loc.end,
  143. },
  144. fix: fixer => {
  145. let start = firstUndefined.range[0];
  146. let end = lastUndefined.range[1];
  147. const previousArgument = argumentNodes[argumentNodes.length - undefinedArguments.length - 1];
  148. if (previousArgument) {
  149. start = previousArgument.range[1];
  150. } else {
  151. // If all arguments removed, and there is trailing comma, we need remove it.
  152. const tokenAfter = sourceCode.getTokenAfter(lastUndefined);
  153. if (isCommaToken(tokenAfter)) {
  154. end = tokenAfter.range[1];
  155. }
  156. }
  157. return fixer.removeRange([start, end]);
  158. },
  159. };
  160. };
  161. }
  162. return listeners;
  163. };
  164. const schema = [
  165. {
  166. type: 'object',
  167. additionalProperties: false,
  168. properties: {
  169. checkArguments: {
  170. type: 'boolean',
  171. },
  172. },
  173. },
  174. ];
  175. /** @type {import('eslint').Rule.RuleModule} */
  176. module.exports = {
  177. create,
  178. meta: {
  179. type: 'suggestion',
  180. docs: {
  181. description: 'Disallow useless `undefined`.',
  182. },
  183. fixable: 'code',
  184. schema,
  185. messages,
  186. },
  187. };