no-useless-undefined.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. 'use strict';
  2. const {isCommaToken} = require('eslint-utils');
  3. const {replaceNodeOrTokenAndSpacesBefore} = require('./fix/index.js');
  4. const {isUndefined} = require('./ast/index.js');
  5. const messageId = 'no-useless-undefined';
  6. const messages = {
  7. [messageId]: 'Do not use useless `undefined`.',
  8. };
  9. const getSelector = (parent, property) =>
  10. `${parent} > Identifier.${property}[name="undefined"]`;
  11. // `return undefined`
  12. const returnSelector = getSelector('ReturnStatement', 'argument');
  13. // `yield undefined`
  14. const yieldSelector = getSelector('YieldExpression[delegate!=true]', 'argument');
  15. // `() => undefined`
  16. const arrowFunctionSelector = getSelector('ArrowFunctionExpression', 'body');
  17. // `let foo = undefined` / `var foo = undefined`
  18. const variableInitSelector = getSelector(
  19. [
  20. 'VariableDeclaration',
  21. '[kind!="const"]',
  22. '>',
  23. 'VariableDeclarator',
  24. ].join(''),
  25. 'init',
  26. );
  27. // `const {foo = undefined} = {}`
  28. const assignmentPatternSelector = getSelector('AssignmentPattern', 'right');
  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. // https://vuejs.org/api/reactivity-core.html#ref
  64. || name === 'ref'
  65. // `set.add(undefined)`
  66. || name === 'add'
  67. // `map.set(foo, undefined)`
  68. || name === 'set'
  69. // `array.push(undefined)`
  70. || name === 'push'
  71. // `array.unshift(undefined)`
  72. || name === 'unshift'
  73. // `React.createContext(undefined)`
  74. || name === 'createContext';
  75. };
  76. const getFunction = scope => {
  77. for (; scope; scope = scope.upper) {
  78. if (scope.type === 'function') {
  79. return scope.block;
  80. }
  81. }
  82. };
  83. const isFunctionBindCall = node =>
  84. !node.optional
  85. && node.callee.type === 'MemberExpression'
  86. && !node.callee.computed
  87. && node.callee.property.type === 'Identifier'
  88. && node.callee.property.name === 'bind';
  89. /** @param {import('eslint').Rule.RuleContext} context */
  90. const create = context => {
  91. const listener = (fix, checkFunctionReturnType) => node => {
  92. if (checkFunctionReturnType) {
  93. const functionNode = getFunction(context.getScope());
  94. if (functionNode && functionNode.returnType) {
  95. return;
  96. }
  97. }
  98. return {
  99. node,
  100. messageId,
  101. fix: fixer => fix(node, fixer),
  102. };
  103. };
  104. const sourceCode = context.getSourceCode();
  105. const options = {
  106. checkArguments: true,
  107. ...context.options[0],
  108. };
  109. const removeNodeAndLeadingSpace = (node, fixer) =>
  110. replaceNodeOrTokenAndSpacesBefore(node, '', fixer, sourceCode);
  111. const listeners = {
  112. [returnSelector]: listener(
  113. removeNodeAndLeadingSpace,
  114. /* CheckFunctionReturnType */ true,
  115. ),
  116. [yieldSelector]: listener(removeNodeAndLeadingSpace),
  117. [arrowFunctionSelector]: listener(
  118. (node, fixer) => replaceNodeOrTokenAndSpacesBefore(node, ' {}', fixer, sourceCode),
  119. /* CheckFunctionReturnType */ true,
  120. ),
  121. [variableInitSelector]: listener(
  122. (node, fixer) => fixer.removeRange([node.parent.id.range[1], node.range[1]]),
  123. ),
  124. [assignmentPatternSelector]: listener(
  125. (node, fixer) => fixer.removeRange([node.parent.left.range[1], node.range[1]]),
  126. ),
  127. };
  128. if (options.checkArguments) {
  129. listeners.CallExpression = node => {
  130. if (shouldIgnore(node.callee)) {
  131. return;
  132. }
  133. const argumentNodes = node.arguments;
  134. // Ignore arguments in `Function#bind()`, but not `this` argument
  135. if (isFunctionBindCall(node) && argumentNodes.length !== 1) {
  136. return;
  137. }
  138. const undefinedArguments = [];
  139. for (let index = argumentNodes.length - 1; index >= 0; index--) {
  140. const node = argumentNodes[index];
  141. if (isUndefined(node)) {
  142. undefinedArguments.unshift(node);
  143. } else {
  144. break;
  145. }
  146. }
  147. if (undefinedArguments.length === 0) {
  148. return;
  149. }
  150. const firstUndefined = undefinedArguments[0];
  151. const lastUndefined = undefinedArguments[undefinedArguments.length - 1];
  152. return {
  153. messageId,
  154. loc: {
  155. start: firstUndefined.loc.start,
  156. end: lastUndefined.loc.end,
  157. },
  158. fix(fixer) {
  159. let start = firstUndefined.range[0];
  160. let end = lastUndefined.range[1];
  161. const previousArgument = argumentNodes[argumentNodes.length - undefinedArguments.length - 1];
  162. if (previousArgument) {
  163. start = previousArgument.range[1];
  164. } else {
  165. // If all arguments removed, and there is trailing comma, we need remove it.
  166. const tokenAfter = sourceCode.getTokenAfter(lastUndefined);
  167. if (isCommaToken(tokenAfter)) {
  168. end = tokenAfter.range[1];
  169. }
  170. }
  171. return fixer.removeRange([start, end]);
  172. },
  173. };
  174. };
  175. }
  176. return listeners;
  177. };
  178. const schema = [
  179. {
  180. type: 'object',
  181. additionalProperties: false,
  182. properties: {
  183. checkArguments: {
  184. type: 'boolean',
  185. },
  186. },
  187. },
  188. ];
  189. /** @type {import('eslint').Rule.RuleModule} */
  190. module.exports = {
  191. create,
  192. meta: {
  193. type: 'suggestion',
  194. docs: {
  195. description: 'Disallow useless `undefined`.',
  196. },
  197. fixable: 'code',
  198. schema,
  199. messages,
  200. },
  201. };