no-useless-undefined.js 5.4 KB

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