parseJestFnCall.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.getNodeChain = getNodeChain;
  6. exports.scopeHasLocalReference = exports.parseJestFnCallWithReason = exports.parseJestFnCall = exports.isTypeOfJestFnCall = void 0;
  7. var _utils = require("@typescript-eslint/utils");
  8. var _utils2 = require("../utils");
  9. const isTypeOfJestFnCall = (node, context, types) => {
  10. const jestFnCall = parseJestFnCall(node, context);
  11. return jestFnCall !== null && types.includes(jestFnCall.type);
  12. };
  13. exports.isTypeOfJestFnCall = isTypeOfJestFnCall;
  14. const joinChains = (a, b) => a && b ? [...a, ...b] : null;
  15. function getNodeChain(node) {
  16. if ((0, _utils2.isSupportedAccessor)(node)) {
  17. return [node];
  18. }
  19. switch (node.type) {
  20. case _utils.AST_NODE_TYPES.TaggedTemplateExpression:
  21. return getNodeChain(node.tag);
  22. case _utils.AST_NODE_TYPES.MemberExpression:
  23. return joinChains(getNodeChain(node.object), getNodeChain(node.property));
  24. case _utils.AST_NODE_TYPES.CallExpression:
  25. return getNodeChain(node.callee);
  26. }
  27. return null;
  28. }
  29. const determineJestFnType = name => {
  30. if (name === 'expect') {
  31. return 'expect';
  32. }
  33. if (name === 'jest') {
  34. return 'jest';
  35. }
  36. if (_utils2.DescribeAlias.hasOwnProperty(name)) {
  37. return 'describe';
  38. }
  39. if (_utils2.TestCaseName.hasOwnProperty(name)) {
  40. return 'test';
  41. }
  42. /* istanbul ignore else */
  43. if (_utils2.HookName.hasOwnProperty(name)) {
  44. return 'hook';
  45. }
  46. /* istanbul ignore next */
  47. return 'unknown';
  48. };
  49. const ValidJestFnCallChains = ['afterAll', 'afterEach', 'beforeAll', 'beforeEach', 'describe', 'describe.each', 'describe.only', 'describe.only.each', 'describe.skip', 'describe.skip.each', 'fdescribe', 'fdescribe.each', 'xdescribe', 'xdescribe.each', 'it', 'it.concurrent', 'it.concurrent.each', 'it.concurrent.only.each', 'it.concurrent.skip.each', 'it.each', 'it.failing', 'it.only', 'it.only.each', 'it.only.failing', 'it.skip', 'it.skip.each', 'it.skip.failing', 'it.todo', 'fit', 'fit.each', 'fit.failing', 'xit', 'xit.each', 'xit.failing', 'test', 'test.concurrent', 'test.concurrent.each', 'test.concurrent.only.each', 'test.concurrent.skip.each', 'test.each', 'test.failing', 'test.only', 'test.only.each', 'test.only.failing', 'test.skip', 'test.skip.each', 'test.skip.failing', 'test.todo', 'xtest', 'xtest.each', 'xtest.failing'];
  50. const resolvePossibleAliasedGlobal = (global, context) => {
  51. var _context$settings$jes, _context$settings$jes2;
  52. const globalAliases = (_context$settings$jes = (_context$settings$jes2 = context.settings.jest) === null || _context$settings$jes2 === void 0 ? void 0 : _context$settings$jes2.globalAliases) !== null && _context$settings$jes !== void 0 ? _context$settings$jes : {};
  53. const alias = Object.entries(globalAliases).find(([, aliases]) => aliases.includes(global));
  54. if (alias) {
  55. return alias[0];
  56. }
  57. return null;
  58. };
  59. const parseJestFnCallCache = new WeakMap();
  60. const parseJestFnCall = (node, context) => {
  61. const jestFnCall = parseJestFnCallWithReason(node, context);
  62. if (typeof jestFnCall === 'string') {
  63. return null;
  64. }
  65. return jestFnCall;
  66. };
  67. exports.parseJestFnCall = parseJestFnCall;
  68. const parseJestFnCallWithReason = (node, context) => {
  69. let parsedJestFnCall = parseJestFnCallCache.get(node);
  70. if (parsedJestFnCall) {
  71. return parsedJestFnCall;
  72. }
  73. parsedJestFnCall = parseJestFnCallWithReasonInner(node, context);
  74. parseJestFnCallCache.set(node, parsedJestFnCall);
  75. return parsedJestFnCall;
  76. };
  77. exports.parseJestFnCallWithReason = parseJestFnCallWithReason;
  78. const parseJestFnCallWithReasonInner = (node, context) => {
  79. var _resolved$original, _node$parent2, _node$parent3;
  80. const chain = getNodeChain(node);
  81. if (!(chain !== null && chain !== void 0 && chain.length)) {
  82. return null;
  83. }
  84. const [first, ...rest] = chain;
  85. const lastLink = (0, _utils2.getAccessorValue)(chain[chain.length - 1]); // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
  86. if (lastLink === 'each') {
  87. if (node.callee.type !== _utils.AST_NODE_TYPES.CallExpression && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
  88. return null;
  89. }
  90. }
  91. if (node.callee.type === _utils.AST_NODE_TYPES.TaggedTemplateExpression && lastLink !== 'each') {
  92. return null;
  93. }
  94. const resolved = resolveToJestFn(context, (0, _utils2.getAccessorValue)(first)); // we're not a jest function
  95. if (!resolved) {
  96. return null;
  97. }
  98. const name = (_resolved$original = resolved.original) !== null && _resolved$original !== void 0 ? _resolved$original : resolved.local;
  99. const links = [name, ...rest.map(link => (0, _utils2.getAccessorValue)(link))];
  100. if (name !== 'jest' && name !== 'expect' && !ValidJestFnCallChains.includes(links.join('.'))) {
  101. return null;
  102. }
  103. const parsedJestFnCall = {
  104. name,
  105. head: { ...resolved,
  106. node: first
  107. },
  108. // every member node must have a member expression as their parent
  109. // in order to be part of the call chain we're parsing
  110. members: rest
  111. };
  112. const type = determineJestFnType(name);
  113. if (type === 'expect') {
  114. const result = parseJestExpectCall(parsedJestFnCall); // if the `expect` call chain is not valid, only report on the topmost node
  115. // since all members in the chain are likely to get flagged for some reason
  116. if (typeof result === 'string' && (0, _utils2.findTopMostCallExpression)(node) !== node) {
  117. return null;
  118. }
  119. if (result === 'matcher-not-found') {
  120. var _node$parent;
  121. if (((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) === _utils.AST_NODE_TYPES.MemberExpression) {
  122. return 'matcher-not-called';
  123. }
  124. }
  125. return result;
  126. } // check that every link in the chain except the last is a member expression
  127. if (chain.slice(0, chain.length - 1).some(nod => {
  128. var _nod$parent;
  129. return ((_nod$parent = nod.parent) === null || _nod$parent === void 0 ? void 0 : _nod$parent.type) !== _utils.AST_NODE_TYPES.MemberExpression;
  130. })) {
  131. return null;
  132. } // ensure that we're at the "top" of the function call chain otherwise when
  133. // parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though
  134. // the full chain is not a valid jest function call chain
  135. if (((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.type) === _utils.AST_NODE_TYPES.CallExpression || ((_node$parent3 = node.parent) === null || _node$parent3 === void 0 ? void 0 : _node$parent3.type) === _utils.AST_NODE_TYPES.MemberExpression) {
  136. return null;
  137. }
  138. return { ...parsedJestFnCall,
  139. type
  140. };
  141. };
  142. const findModifiersAndMatcher = members => {
  143. const modifiers = [];
  144. for (const member of members) {
  145. var _member$parent, _member$parent$parent;
  146. // check if the member is being called, which means it is the matcher
  147. // (and also the end of the entire "expect" call chain)
  148. if (((_member$parent = member.parent) === null || _member$parent === void 0 ? void 0 : _member$parent.type) === _utils.AST_NODE_TYPES.MemberExpression && ((_member$parent$parent = member.parent.parent) === null || _member$parent$parent === void 0 ? void 0 : _member$parent$parent.type) === _utils.AST_NODE_TYPES.CallExpression) {
  149. return {
  150. matcher: member,
  151. args: member.parent.parent.arguments,
  152. modifiers
  153. };
  154. } // otherwise, it should be a modifier
  155. const name = (0, _utils2.getAccessorValue)(member);
  156. if (modifiers.length === 0) {
  157. // the first modifier can be any of the three modifiers
  158. if (!_utils2.ModifierName.hasOwnProperty(name)) {
  159. return 'modifier-unknown';
  160. }
  161. } else if (modifiers.length === 1) {
  162. // the second modifier can only be "not"
  163. if (name !== _utils2.ModifierName.not) {
  164. return 'modifier-unknown';
  165. }
  166. const firstModifier = (0, _utils2.getAccessorValue)(modifiers[0]); // and the first modifier has to be either "resolves" or "rejects"
  167. if (firstModifier !== _utils2.ModifierName.resolves && firstModifier !== _utils2.ModifierName.rejects) {
  168. return 'modifier-unknown';
  169. }
  170. } else {
  171. return 'modifier-unknown';
  172. }
  173. modifiers.push(member);
  174. } // this will only really happen if there are no members
  175. return 'matcher-not-found';
  176. };
  177. const parseJestExpectCall = typelessParsedJestFnCall => {
  178. const modifiersAndMatcher = findModifiersAndMatcher(typelessParsedJestFnCall.members);
  179. if (typeof modifiersAndMatcher === 'string') {
  180. return modifiersAndMatcher;
  181. }
  182. return { ...typelessParsedJestFnCall,
  183. type: 'expect',
  184. ...modifiersAndMatcher
  185. };
  186. };
  187. const describeImportDefAsImport = def => {
  188. if (def.parent.type === _utils.AST_NODE_TYPES.TSImportEqualsDeclaration) {
  189. return null;
  190. }
  191. if (def.node.type !== _utils.AST_NODE_TYPES.ImportSpecifier) {
  192. return null;
  193. } // we only care about value imports
  194. if (def.parent.importKind === 'type') {
  195. return null;
  196. }
  197. return {
  198. source: def.parent.source.value,
  199. imported: def.node.imported.name,
  200. local: def.node.local.name
  201. };
  202. };
  203. /**
  204. * Attempts to find the node that represents the import source for the
  205. * given expression node, if it looks like it's an import.
  206. *
  207. * If no such node can be found (e.g. because the expression doesn't look
  208. * like an import), then `null` is returned instead.
  209. */
  210. const findImportSourceNode = node => {
  211. if (node.type === _utils.AST_NODE_TYPES.AwaitExpression) {
  212. if (node.argument.type === _utils.AST_NODE_TYPES.ImportExpression) {
  213. return node.argument.source;
  214. }
  215. return null;
  216. }
  217. if (node.type === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.isIdentifier)(node.callee, 'require')) {
  218. var _node$arguments$;
  219. return (_node$arguments$ = node.arguments[0]) !== null && _node$arguments$ !== void 0 ? _node$arguments$ : null;
  220. }
  221. return null;
  222. };
  223. const describeVariableDefAsImport = def => {
  224. var _def$name$parent;
  225. // make sure that we've actually being assigned a value
  226. if (!def.node.init) {
  227. return null;
  228. }
  229. const sourceNode = findImportSourceNode(def.node.init);
  230. if (!sourceNode || !(0, _utils2.isStringNode)(sourceNode)) {
  231. return null;
  232. }
  233. if (((_def$name$parent = def.name.parent) === null || _def$name$parent === void 0 ? void 0 : _def$name$parent.type) !== _utils.AST_NODE_TYPES.Property) {
  234. return null;
  235. }
  236. if (!(0, _utils2.isSupportedAccessor)(def.name.parent.key)) {
  237. return null;
  238. }
  239. return {
  240. source: (0, _utils2.getStringValue)(sourceNode),
  241. imported: (0, _utils2.getAccessorValue)(def.name.parent.key),
  242. local: def.name.name
  243. };
  244. };
  245. /**
  246. * Attempts to describe a definition as an import if possible.
  247. *
  248. * If the definition is an import binding, it's described as you'd expect.
  249. * If the definition is a variable, then we try and determine if it's either
  250. * a dynamic `import()` or otherwise a call to `require()`.
  251. *
  252. * If it's neither of these, `null` is returned to indicate that the definition
  253. * is not describable as an import of any kind.
  254. */
  255. const describePossibleImportDef = def => {
  256. if (def.type === 'Variable') {
  257. return describeVariableDefAsImport(def);
  258. }
  259. if (def.type === 'ImportBinding') {
  260. return describeImportDefAsImport(def);
  261. }
  262. return null;
  263. };
  264. const collectReferences = scope => {
  265. const locals = new Set();
  266. const imports = new Map();
  267. const unresolved = new Set();
  268. let currentScope = scope;
  269. while (currentScope !== null) {
  270. for (const ref of currentScope.variables) {
  271. if (ref.defs.length === 0) {
  272. continue;
  273. }
  274. const def = ref.defs[ref.defs.length - 1];
  275. const importDetails = describePossibleImportDef(def);
  276. if (importDetails) {
  277. imports.set(importDetails.local, importDetails);
  278. continue;
  279. }
  280. locals.add(ref.name);
  281. }
  282. for (const ref of currentScope.through) {
  283. unresolved.add(ref.identifier.name);
  284. }
  285. currentScope = currentScope.upper;
  286. }
  287. return {
  288. locals,
  289. imports,
  290. unresolved
  291. };
  292. };
  293. const resolveToJestFn = (context, identifier) => {
  294. const references = collectReferences(context.getScope());
  295. const maybeImport = references.imports.get(identifier);
  296. if (maybeImport) {
  297. // the identifier is imported from @jest/globals,
  298. // so return the original import name
  299. if (maybeImport.source === '@jest/globals') {
  300. return {
  301. original: maybeImport.imported,
  302. local: maybeImport.local,
  303. type: 'import'
  304. };
  305. }
  306. return null;
  307. } // the identifier was found as a local variable or function declaration
  308. // meaning it's not a function from jest
  309. if (references.locals.has(identifier)) {
  310. return null;
  311. }
  312. return {
  313. original: resolvePossibleAliasedGlobal(identifier, context),
  314. local: identifier,
  315. type: 'global'
  316. };
  317. };
  318. const scopeHasLocalReference = (scope, referenceName) => {
  319. const references = collectReferences(scope);
  320. return (// referenceName was found as a local variable or function declaration.
  321. references.locals.has(referenceName) || // referenceName was found as an imported identifier
  322. references.imports.has(referenceName) || // referenceName was not found as an unresolved reference,
  323. // meaning it is likely not an implicit global reference.
  324. !references.unresolved.has(referenceName)
  325. );
  326. };
  327. exports.scopeHasLocalReference = scopeHasLocalReference;