diff --git a/README.md b/README.md index 14d603b5c..799242428 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,25 @@ doing: This is included in all configs shared by this plugin, so can be omitted if extending them. +#### Aliased Jest globals + +You can tell this plugin about any global Jests you have aliased using the +`globalAliases` setting: + +```json +{ + "settings": { + "jest": { + "globalAliases": { + "describe": ["context"], + "fdescribe": ["fcontext"], + "xdescribe": ["xcontext"] + } + } + } +} +``` + ### Running rules only on test-related files The rules provided by this plugin assume that the files they are checking are diff --git a/src/rules/__tests__/no-focused-tests.test.ts b/src/rules/__tests__/no-focused-tests.test.ts index 2e746b149..c98fc903d 100644 --- a/src/rules/__tests__/no-focused-tests.test.ts +++ b/src/rules/__tests__/no-focused-tests.test.ts @@ -44,6 +44,23 @@ ruleTester.run('no-focused-tests', rule, { }, ], }, + { + code: 'context.only()', + errors: [ + { + messageId: 'focusedTest', + column: 9, + line: 1, + suggestions: [ + { + messageId: 'suggestRemoveFocus', + output: 'context()', + }, + ], + }, + ], + settings: { jest: { globalAliases: { describe: ['context'] } } }, + }, { code: 'describe.only.each()()', errors: [ diff --git a/src/rules/__tests__/no-identical-title.test.ts b/src/rules/__tests__/no-identical-title.test.ts index 70321e36a..28c1e011e 100644 --- a/src/rules/__tests__/no-identical-title.test.ts +++ b/src/rules/__tests__/no-identical-title.test.ts @@ -243,5 +243,15 @@ ruleTester.run('no-identical-title', rule, { `, errors: [{ messageId: 'multipleTestTitle', column: 6, line: 3 }], }, + { + code: dedent` + context('foo', () => { + describe('foe', () => {}); + }); + describe('foo', () => {}); + `, + errors: [{ messageId: 'multipleDescribeTitle', column: 10, line: 4 }], + settings: { jest: { globalAliases: { describe: ['context'] } } }, + }, ], }); diff --git a/src/rules/__tests__/valid-expect-in-promise.test.ts b/src/rules/__tests__/valid-expect-in-promise.test.ts index a070f23ff..d5dfb338d 100644 --- a/src/rules/__tests__/valid-expect-in-promise.test.ts +++ b/src/rules/__tests__/valid-expect-in-promise.test.ts @@ -1613,5 +1613,26 @@ ruleTester.run('valid-expect-in-promise', rule, { }, ], }, + { + code: dedent` + promiseThatThis('is valid', async () => { + const promise = loadNumber().then(number => { + expect(typeof number).toBe('number'); + + return number + 1; + }); + + expect(anotherPromise).resolves.toBe(1); + }); + `, + errors: [ + { + messageId: 'expectInFloatingPromise', + line: 2, + column: 9, + }, + ], + settings: { jest: { globalAliases: { xit: ['promiseThatThis'] } } }, + }, ], }); diff --git a/src/rules/consistent-test-it.ts b/src/rules/consistent-test-it.ts index 91975354a..6f0705d16 100644 --- a/src/rules/consistent-test-it.ts +++ b/src/rules/consistent-test-it.ts @@ -72,8 +72,7 @@ export default createRule< return { CallExpression(node: TSESTree.CallExpression) { - const scope = context.getScope(); - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (!jestFnCall) { return; @@ -129,7 +128,7 @@ export default createRule< } }, 'CallExpression:exit'(node) { - if (isTypeOfJestFnCall(node, context.getScope(), ['describe'])) { + if (isTypeOfJestFnCall(node, context, ['describe'])) { describeNestingLevel--; } }, diff --git a/src/rules/expect-expect.ts b/src/rules/expect-expect.ts index 08a836e4a..f6f4cbce9 100644 --- a/src/rules/expect-expect.ts +++ b/src/rules/expect-expect.ts @@ -96,7 +96,7 @@ export default createRule< const testCallExpressions = getTestCallExpressionsFromDeclaredVariables( declaredVariables, - context.getScope(), + context, ); checkCallExpressionUsed(testCallExpressions); @@ -114,7 +114,7 @@ export default createRule< const name = getNodeName(node.callee) ?? ''; if ( - isTypeOfJestFnCall(node, context.getScope(), ['test']) || + isTypeOfJestFnCall(node, context, ['test']) || additionalTestBlockFunctions.includes(name) ) { if ( diff --git a/src/rules/max-nested-describe.ts b/src/rules/max-nested-describe.ts index dfa1f4c82..d40c0940e 100644 --- a/src/rules/max-nested-describe.ts +++ b/src/rules/max-nested-describe.ts @@ -38,7 +38,7 @@ export default createRule({ if ( parent?.type !== AST_NODE_TYPES.CallExpression || - !isTypeOfJestFnCall(parent, context.getScope(), ['describe']) + !isTypeOfJestFnCall(parent, context, ['describe']) ) { return; } @@ -61,7 +61,7 @@ export default createRule({ if ( parent?.type === AST_NODE_TYPES.CallExpression && - isTypeOfJestFnCall(parent, context.getScope(), ['describe']) + isTypeOfJestFnCall(parent, context, ['describe']) ) { describeCallbackStack.pop(); } diff --git a/src/rules/no-conditional-expect.ts b/src/rules/no-conditional-expect.ts index 96a7a424f..04b0a69cc 100644 --- a/src/rules/no-conditional-expect.ts +++ b/src/rules/no-conditional-expect.ts @@ -42,7 +42,7 @@ export default createRule({ const declaredVariables = context.getDeclaredVariables(node); const testCallExpressions = getTestCallExpressionsFromDeclaredVariables( declaredVariables, - context.getScope(), + context, ); if (testCallExpressions.length > 0) { @@ -50,7 +50,7 @@ export default createRule({ } }, CallExpression(node: TSESTree.CallExpression) { - if (isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (isTypeOfJestFnCall(node, context, ['test'])) { inTestCase = true; } @@ -73,7 +73,7 @@ export default createRule({ } }, 'CallExpression:exit'(node) { - if (isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (isTypeOfJestFnCall(node, context, ['test'])) { inTestCase = false; } diff --git a/src/rules/no-conditional-in-test.ts b/src/rules/no-conditional-in-test.ts index 354bb9410..f6ad78fbf 100644 --- a/src/rules/no-conditional-in-test.ts +++ b/src/rules/no-conditional-in-test.ts @@ -30,12 +30,12 @@ export default createRule({ return { CallExpression(node: TSESTree.CallExpression) { - if (isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (isTypeOfJestFnCall(node, context, ['test'])) { inTestCase = true; } }, 'CallExpression:exit'(node) { - if (isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (isTypeOfJestFnCall(node, context, ['test'])) { inTestCase = false; } }, diff --git a/src/rules/no-disabled-tests.ts b/src/rules/no-disabled-tests.ts index e8c779c9d..f4300659e 100644 --- a/src/rules/no-disabled-tests.ts +++ b/src/rules/no-disabled-tests.ts @@ -31,7 +31,7 @@ export default createRule({ return { CallExpression(node) { - const jestFnCall = parseJestFnCall(node, context.getScope()); + const jestFnCall = parseJestFnCall(node, context); if (!jestFnCall) { return; @@ -65,7 +65,7 @@ export default createRule({ } }, 'CallExpression:exit'(node) { - const jestFnCall = parseJestFnCall(node, context.getScope()); + const jestFnCall = parseJestFnCall(node, context); if (!jestFnCall) { return; diff --git a/src/rules/no-done-callback.ts b/src/rules/no-done-callback.ts index c2fda6063..366745d64 100644 --- a/src/rules/no-done-callback.ts +++ b/src/rules/no-done-callback.ts @@ -4,13 +4,13 @@ import { createRule, getNodeName, isFunction, parseJestFnCall } from './utils'; const findCallbackArg = ( node: TSESTree.CallExpression, isJestEach: boolean, - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, ): TSESTree.CallExpression['arguments'][0] | null => { if (isJestEach) { return node.arguments[1]; } - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall?.type === 'hook' && node.arguments.length >= 1) { return node.arguments[0]; @@ -60,7 +60,7 @@ export default createRule({ return; } - const callback = findCallbackArg(node, isJestEach, context.getScope()); + const callback = findCallbackArg(node, isJestEach, context); const callbackArgIndex = Number(isJestEach); if ( diff --git a/src/rules/no-duplicate-hooks.ts b/src/rules/no-duplicate-hooks.ts index 497cb6b88..b47db957b 100644 --- a/src/rules/no-duplicate-hooks.ts +++ b/src/rules/no-duplicate-hooks.ts @@ -20,9 +20,7 @@ export default createRule({ return { CallExpression(node) { - const scope = context.getScope(); - - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall?.type === 'describe') { hookContexts.push({}); @@ -45,7 +43,7 @@ export default createRule({ } }, 'CallExpression:exit'(node) { - if (isTypeOfJestFnCall(node, context.getScope(), ['describe'])) { + if (isTypeOfJestFnCall(node, context, ['describe'])) { hookContexts.pop(); } }, diff --git a/src/rules/no-export.ts b/src/rules/no-export.ts index d35fd9af0..eb8245b93 100644 --- a/src/rules/no-export.ts +++ b/src/rules/no-export.ts @@ -34,7 +34,7 @@ export default createRule({ }, CallExpression(node) { - if (isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (isTypeOfJestFnCall(node, context, ['test'])) { hasTestCase = true; } }, diff --git a/src/rules/no-focused-tests.ts b/src/rules/no-focused-tests.ts index 2010cf3f7..cea4fb325 100644 --- a/src/rules/no-focused-tests.ts +++ b/src/rules/no-focused-tests.ts @@ -22,9 +22,7 @@ export default createRule({ create(context) { return { CallExpression(node) { - const scope = context.getScope(); - - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall?.type !== 'test' && jestFnCall?.type !== 'describe') { return; diff --git a/src/rules/no-hooks.ts b/src/rules/no-hooks.ts index 778d3095f..cf4064fd4 100644 --- a/src/rules/no-hooks.ts +++ b/src/rules/no-hooks.ts @@ -32,7 +32,7 @@ export default createRule< create(context, [{ allow = [] }]) { return { CallExpression(node) { - const jestFnCall = parseJestFnCall(node, context.getScope()); + const jestFnCall = parseJestFnCall(node, context); if ( jestFnCall?.type === 'hook' && diff --git a/src/rules/no-identical-title.ts b/src/rules/no-identical-title.ts index c013c93fa..11cefaaf2 100644 --- a/src/rules/no-identical-title.ts +++ b/src/rules/no-identical-title.ts @@ -40,10 +40,9 @@ export default createRule({ return { CallExpression(node) { - const scope = context.getScope(); const currentLayer = contexts[contexts.length - 1]; - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (!jestFnCall) { return; @@ -87,7 +86,7 @@ export default createRule({ currentLayer.describeTitles.push(title); }, 'CallExpression:exit'(node) { - if (isTypeOfJestFnCall(node, context.getScope(), ['describe'])) { + if (isTypeOfJestFnCall(node, context, ['describe'])) { contexts.pop(); } }, diff --git a/src/rules/no-if.ts b/src/rules/no-if.ts index dd651850b..af3defd7f 100644 --- a/src/rules/no-if.ts +++ b/src/rules/no-if.ts @@ -75,7 +75,7 @@ export default createRule({ return { CallExpression(node) { - const jestFnCall = parseJestFnCall(node, context.getScope()); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall?.type === 'test') { stack.push(true); @@ -92,7 +92,7 @@ export default createRule({ const declaredVariables = context.getDeclaredVariables(node); const testCallExpressions = getTestCallExpressionsFromDeclaredVariables( declaredVariables, - context.getScope(), + context, ); stack.push(testCallExpressions.length > 0); diff --git a/src/rules/no-standalone-expect.ts b/src/rules/no-standalone-expect.ts index 26fc50710..4bb852fd1 100644 --- a/src/rules/no-standalone-expect.ts +++ b/src/rules/no-standalone-expect.ts @@ -10,7 +10,7 @@ import { const getBlockType = ( statement: TSESTree.BlockStatement, - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, ): 'function' | 'describe' | null => { const func = statement.parent; @@ -37,7 +37,7 @@ const getBlockType = ( // if it's not a variable, it will be callExpr, we only care about describe if ( expr.type === AST_NODE_TYPES.CallExpression && - isTypeOfJestFnCall(expr, scope, ['describe']) + isTypeOfJestFnCall(expr, context, ['describe']) ) { return 'describe'; } @@ -85,7 +85,7 @@ export default createRule< additionalTestBlockFunctions.includes(getNodeName(node) || ''); const isTestBlock = (node: TSESTree.CallExpression): boolean => - isTypeOfJestFnCall(node, context.getScope(), ['test']) || + isTypeOfJestFnCall(node, context, ['test']) || isCustomTestBlockFunction(node); return { @@ -123,7 +123,7 @@ export default createRule< }, BlockStatement(statement) { - const blockType = getBlockType(statement, context.getScope()); + const blockType = getBlockType(statement, context); if (blockType) { callStack.push(blockType); @@ -131,8 +131,7 @@ export default createRule< }, 'BlockStatement:exit'(statement: TSESTree.BlockStatement) { if ( - callStack[callStack.length - 1] === - getBlockType(statement, context.getScope()) + callStack[callStack.length - 1] === getBlockType(statement, context) ) { callStack.pop(); } diff --git a/src/rules/no-test-prefixes.ts b/src/rules/no-test-prefixes.ts index 23f8941b9..dcd5fbf42 100644 --- a/src/rules/no-test-prefixes.ts +++ b/src/rules/no-test-prefixes.ts @@ -20,8 +20,7 @@ export default createRule({ create(context) { return { CallExpression(node) { - const scope = context.getScope(); - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall?.type !== 'describe' && jestFnCall?.type !== 'test') { return; diff --git a/src/rules/no-test-return-statement.ts b/src/rules/no-test-return-statement.ts index ce1d6b290..845e41d6e 100644 --- a/src/rules/no-test-return-statement.ts +++ b/src/rules/no-test-return-statement.ts @@ -38,7 +38,7 @@ export default createRule({ create(context) { return { CallExpression(node) { - if (!isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (!isTypeOfJestFnCall(node, context, ['test'])) { return; } @@ -55,7 +55,7 @@ export default createRule({ const declaredVariables = context.getDeclaredVariables(node); const testCallExpressions = getTestCallExpressionsFromDeclaredVariables( declaredVariables, - context.getScope(), + context, ); if (testCallExpressions.length === 0) return; diff --git a/src/rules/prefer-expect-assertions.ts b/src/rules/prefer-expect-assertions.ts index 64cb49c42..db1d60553 100644 --- a/src/rules/prefer-expect-assertions.ts +++ b/src/rules/prefer-expect-assertions.ts @@ -157,7 +157,7 @@ export default createRule<[RuleOptions], MessageIds>({ ForOfStatement: enterForLoop, 'ForOfStatement:exit': exitForLoop, CallExpression(node) { - if (isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (isTypeOfJestFnCall(node, context, ['test'])) { inTestCaseCall = true; return; @@ -174,7 +174,7 @@ export default createRule<[RuleOptions], MessageIds>({ } }, 'CallExpression:exit'(node: TSESTree.CallExpression) { - if (!isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (!isTypeOfJestFnCall(node, context, ['test'])) { return; } diff --git a/src/rules/prefer-hooks-in-order.ts b/src/rules/prefer-hooks-in-order.ts index 5a449f0ac..3f1f626d1 100644 --- a/src/rules/prefer-hooks-in-order.ts +++ b/src/rules/prefer-hooks-in-order.ts @@ -28,7 +28,7 @@ export default createRule({ return; } - const jestFnCall = parseJestFnCall(node, context.getScope()); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall?.type !== 'hook') { // Reset the previousHookIndex when encountering something different from a hook @@ -57,7 +57,7 @@ export default createRule({ previousHookIndex = currentHookIndex; }, 'CallExpression:exit'(node) { - if (isTypeOfJestFnCall(node, context.getScope(), ['hook'])) { + if (isTypeOfJestFnCall(node, context, ['hook'])) { inHook = false; return; diff --git a/src/rules/prefer-hooks-on-top.ts b/src/rules/prefer-hooks-on-top.ts index 9b024747e..433e20a9b 100644 --- a/src/rules/prefer-hooks-on-top.ts +++ b/src/rules/prefer-hooks-on-top.ts @@ -20,14 +20,12 @@ export default createRule({ return { CallExpression(node) { - const scope = context.getScope(); - - if (isTypeOfJestFnCall(node, scope, ['test'])) { + if (isTypeOfJestFnCall(node, context, ['test'])) { hooksContext[hooksContext.length - 1] = true; } if ( hooksContext[hooksContext.length - 1] && - isTypeOfJestFnCall(node, scope, ['hook']) + isTypeOfJestFnCall(node, context, ['hook']) ) { context.report({ messageId: 'noHookOnTop', diff --git a/src/rules/prefer-lowercase-title.ts b/src/rules/prefer-lowercase-title.ts index d2deba93e..ccc077596 100644 --- a/src/rules/prefer-lowercase-title.ts +++ b/src/rules/prefer-lowercase-title.ts @@ -104,9 +104,7 @@ export default createRule< return { CallExpression(node: TSESTree.CallExpression) { - const scope = context.getScope(); - - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (!jestFnCall || !hasStringAsFirstArgument(node)) { return; @@ -162,7 +160,7 @@ export default createRule< }); }, 'CallExpression:exit'(node: TSESTree.CallExpression) { - if (isTypeOfJestFnCall(node, context.getScope(), ['describe'])) { + if (isTypeOfJestFnCall(node, context, ['describe'])) { numberOfDescribeBlocks--; } }, diff --git a/src/rules/prefer-snapshot-hint.ts b/src/rules/prefer-snapshot-hint.ts index e418fac2f..b14f5a403 100644 --- a/src/rules/prefer-snapshot-hint.ts +++ b/src/rules/prefer-snapshot-hint.ts @@ -106,17 +106,13 @@ export default createRule<[('always' | 'multi')?], keyof typeof messages>({ ArrowFunctionExpression: enterExpression, 'ArrowFunctionExpression:exit': exitExpression, 'CallExpression:exit'(node) { - const scope = context.getScope(); - - if (isTypeOfJestFnCall(node, scope, ['describe', 'test'])) { + if (isTypeOfJestFnCall(node, context, ['describe', 'test'])) { /* istanbul ignore next */ expressionDepth = depths.pop() ?? 0; } }, CallExpression(node) { - const scope = context.getScope(); - - if (isTypeOfJestFnCall(node, scope, ['describe', 'test'])) { + if (isTypeOfJestFnCall(node, context, ['describe', 'test'])) { depths.push(expressionDepth); expressionDepth = 0; } diff --git a/src/rules/prefer-todo.ts b/src/rules/prefer-todo.ts index 0954ec9fd..0711b20b2 100644 --- a/src/rules/prefer-todo.ts +++ b/src/rules/prefer-todo.ts @@ -69,7 +69,7 @@ export default createRule({ CallExpression(node) { const [title, callback] = node.arguments; - const jestFnCall = parseJestFnCall(node, context.getScope()); + const jestFnCall = parseJestFnCall(node, context); if ( !title || diff --git a/src/rules/require-hook.ts b/src/rules/require-hook.ts index 79e43fec4..134aca5c7 100644 --- a/src/rules/require-hook.ts +++ b/src/rules/require-hook.ts @@ -10,9 +10,9 @@ import { const isJestFnCall = ( node: TSESTree.CallExpression, - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, ): boolean => { - if (parseJestFnCall(node, scope)) { + if (parseJestFnCall(node, context)) { return true; } @@ -28,15 +28,15 @@ const isNullOrUndefined = (node: TSESTree.Expression): boolean => { const shouldBeInHook = ( node: TSESTree.Node, - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, allowedFunctionCalls: readonly string[] = [], ): boolean => { switch (node.type) { case AST_NODE_TYPES.ExpressionStatement: - return shouldBeInHook(node.expression, scope, allowedFunctionCalls); + return shouldBeInHook(node.expression, context, allowedFunctionCalls); case AST_NODE_TYPES.CallExpression: return !( - isJestFnCall(node, scope) || + isJestFnCall(node, context) || allowedFunctionCalls.includes(getNodeName(node) as string) ); case AST_NODE_TYPES.VariableDeclaration: { @@ -92,9 +92,7 @@ export default createRule< const checkBlockBody = (body: TSESTree.BlockStatement['body']) => { for (const statement of body) { - if ( - shouldBeInHook(statement, context.getScope(), allowedFunctionCalls) - ) { + if (shouldBeInHook(statement, context, allowedFunctionCalls)) { context.report({ node: statement, messageId: 'useHook', @@ -109,7 +107,7 @@ export default createRule< }, CallExpression(node) { if ( - !isTypeOfJestFnCall(node, context.getScope(), ['describe']) || + !isTypeOfJestFnCall(node, context, ['describe']) || node.arguments.length < 2 ) { return; diff --git a/src/rules/require-top-level-describe.ts b/src/rules/require-top-level-describe.ts index 1d57bc1a8..e5e4c3d03 100644 --- a/src/rules/require-top-level-describe.ts +++ b/src/rules/require-top-level-describe.ts @@ -44,9 +44,7 @@ export default createRule< return { CallExpression(node) { - const scope = context.getScope(); - - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (!jestFnCall) { return; @@ -87,7 +85,7 @@ export default createRule< } }, 'CallExpression:exit'(node: TSESTree.CallExpression) { - if (isTypeOfJestFnCall(node, context.getScope(), ['describe'])) { + if (isTypeOfJestFnCall(node, context, ['describe'])) { numberOfDescribeBlocks--; } }, diff --git a/src/rules/utils/__tests__/parseJestFnCall.test.ts b/src/rules/utils/__tests__/parseJestFnCall.test.ts index b4872e24c..fadf9ed87 100644 --- a/src/rules/utils/__tests__/parseJestFnCall.test.ts +++ b/src/rules/utils/__tests__/parseJestFnCall.test.ts @@ -61,7 +61,7 @@ const rule = createRule({ defaultOptions: [], create: context => ({ CallExpression(node) { - const jestFnCall = parseJestFnCall(node, context.getScope()); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall) { context.report({ @@ -331,6 +331,128 @@ ruleTester.run('cjs', rule, { invalid: [], }); +ruleTester.run('global aliases', rule, { + valid: [ + { + code: 'xcontext("skip this please", () => {});', + settings: { jest: { globalAliases: { describe: ['context'] } } }, + }, + ], + invalid: [ + { + code: 'context("when there is an error", () => {})', + errors: [ + { + messageId: 'details' as const, + data: expectedParsedJestFnCallResultData({ + name: 'describe', + type: 'describe', + head: { + original: 'describe', + local: 'context', + type: 'global', + node: 'context', + }, + members: [], + }), + column: 1, + line: 1, + }, + ], + settings: { jest: { globalAliases: { describe: ['context'] } } }, + }, + { + code: 'context.skip("when there is an error", () => {})', + errors: [ + { + messageId: 'details' as const, + data: expectedParsedJestFnCallResultData({ + name: 'describe', + type: 'describe', + head: { + original: 'describe', + local: 'context', + type: 'global', + node: 'context', + }, + members: ['skip'], + }), + column: 1, + line: 1, + }, + ], + settings: { jest: { globalAliases: { describe: ['context'] } } }, + }, + { + code: dedent` + context("when there is an error", () => {}) + xcontext("skip this please", () => {}); + `, + errors: [ + { + messageId: 'details' as const, + data: expectedParsedJestFnCallResultData({ + name: 'xdescribe', + type: 'describe', + head: { + original: 'xdescribe', + local: 'xcontext', + type: 'global', + node: 'xcontext', + }, + members: [], + }), + column: 1, + line: 2, + }, + ], + settings: { jest: { globalAliases: { xdescribe: ['xcontext'] } } }, + }, + { + code: dedent` + context("when there is an error", () => {}) + describe("when there is an error", () => {}) + xcontext("skip this please", () => {}); + `, + errors: [ + { + messageId: 'details' as const, + data: expectedParsedJestFnCallResultData({ + name: 'describe', + type: 'describe', + head: { + original: 'describe', + local: 'context', + type: 'global', + node: 'context', + }, + members: [], + }), + column: 1, + line: 1, + }, + { + messageId: 'details' as const, + data: expectedParsedJestFnCallResultData({ + name: 'describe', + type: 'describe', + head: { + original: null, + local: 'describe', + type: 'global', + node: 'describe', + }, + members: [], + }), + column: 1, + line: 2, + }, + ], + settings: { jest: { globalAliases: { describe: ['context'] } } }, + }, + ], +}); + ruleTester.run('typescript', rule, { valid: [ { diff --git a/src/rules/utils/misc.ts b/src/rules/utils/misc.ts index 2039d0053..6533d8872 100644 --- a/src/rules/utils/misc.ts +++ b/src/rules/utils/misc.ts @@ -121,7 +121,7 @@ export const isFunction = (node: TSESTree.Node): node is FunctionExpression => export const getTestCallExpressionsFromDeclaredVariables = ( declaredVariables: readonly TSESLint.Scope.Variable[], - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, ): TSESTree.CallExpression[] => { return declaredVariables.reduce( (acc, { references }) => @@ -131,7 +131,7 @@ export const getTestCallExpressionsFromDeclaredVariables = ( .filter( (node): node is TSESTree.CallExpression => node?.type === AST_NODE_TYPES.CallExpression && - isTypeOfJestFnCall(node, scope, ['test']), + isTypeOfJestFnCall(node, context, ['test']), ), ), [], diff --git a/src/rules/utils/parseJestFnCall.ts b/src/rules/utils/parseJestFnCall.ts index 15fd18aa2..b1f2c4ccb 100644 --- a/src/rules/utils/parseJestFnCall.ts +++ b/src/rules/utils/parseJestFnCall.ts @@ -13,10 +13,10 @@ import { export const isTypeOfJestFnCall = ( node: TSESTree.CallExpression, - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, types: JestFnType[], ): boolean => { - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); return jestFnCall !== null && types.includes(jestFnCall.type); }; @@ -140,9 +140,35 @@ const ValidJestFnCallChains = [ 'xtest.failing', ]; +declare module '@typescript-eslint/utils/dist/ts-eslint' { + export interface SharedConfigurationSettings { + jest?: { + globalAliases?: Record; + version?: number | string; + }; + } +} + +const resolvePossibleAliasedGlobal = ( + global: string, + context: TSESLint.RuleContext, +) => { + const globalAliases = context.settings.jest?.globalAliases ?? {}; + + const alias = Object.entries(globalAliases).find(([, aliases]) => + aliases.includes(global), + ); + + if (alias) { + return alias[0]; + } + + return null; +}; + export const parseJestFnCall = ( node: TSESTree.CallExpression, - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, ): ParsedJestFnCall | null => { // ensure that we're at the "top" of the function call chain otherwise when // parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though @@ -190,7 +216,7 @@ export const parseJestFnCall = ( return null; } - const resolved = resolveToJestFn(scope, getAccessorValue(first)); + const resolved = resolveToJestFn(context, getAccessorValue(first)); // we're not a jest function if (!resolved) { @@ -364,10 +390,10 @@ interface ResolvedJestFn { } const resolveToJestFn = ( - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, identifier: string, ): ResolvedJestFn | null => { - const references = collectReferences(scope); + const references = collectReferences(context.getScope()); const maybeImport = references.imports.get(identifier); @@ -392,7 +418,7 @@ const resolveToJestFn = ( } return { - original: null, + original: resolvePossibleAliasedGlobal(identifier, context), local: identifier, type: 'global', }; diff --git a/src/rules/valid-describe-callback.ts b/src/rules/valid-describe-callback.ts index 88c4fb70c..fed365928 100644 --- a/src/rules/valid-describe-callback.ts +++ b/src/rules/valid-describe-callback.ts @@ -41,7 +41,7 @@ export default createRule({ create(context) { return { CallExpression(node) { - const jestFnCall = parseJestFnCall(node, context.getScope()); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall?.type !== 'describe') { return; diff --git a/src/rules/valid-expect-in-promise.ts b/src/rules/valid-expect-in-promise.ts index a87400cf7..9ffc59da6 100644 --- a/src/rules/valid-expect-in-promise.ts +++ b/src/rules/valid-expect-in-promise.ts @@ -70,9 +70,9 @@ const findTopMostCallExpression = ( const isTestCaseCallWithCallbackArg = ( node: TSESTree.CallExpression, - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, ): boolean => { - const jestCallFn = parseJestFnCall(node, scope); + const jestCallFn = parseJestFnCall(node, context); if (jestCallFn?.type !== 'test') { return false; @@ -324,7 +324,7 @@ const findFirstBlockBodyUp = ( const isDirectlyWithinTestCaseCall = ( node: TSESTree.Node, - scope: TSESLint.Scope.Scope, + context: TSESLint.RuleContext, ): boolean => { let parent: TSESTree.Node['parent'] = node; @@ -334,7 +334,7 @@ const isDirectlyWithinTestCaseCall = ( return ( parent?.type === AST_NODE_TYPES.CallExpression && - isTypeOfJestFnCall(parent, scope, ['test']) + isTypeOfJestFnCall(parent, context, ['test']) ); } @@ -390,7 +390,7 @@ export default createRule({ CallExpression(node: TSESTree.CallExpression) { // there are too many ways that the done argument could be used with // promises that contain expect that would make the promise safe for us - if (isTestCaseCallWithCallbackArg(node, context.getScope())) { + if (isTestCaseCallWithCallbackArg(node, context)) { inTestCaseWithDoneCallback = true; return; @@ -415,7 +415,7 @@ export default createRule({ // make promises containing expects safe in a test for us to be able to // accurately check, so we just bail out completely if it's present if (inTestCaseWithDoneCallback) { - if (isTypeOfJestFnCall(node, context.getScope(), ['test'])) { + if (isTypeOfJestFnCall(node, context, ['test'])) { inTestCaseWithDoneCallback = false; } @@ -442,10 +442,7 @@ export default createRule({ // or our parent is not directly within the test case, we stop checking // because we're most likely in the body of a function being defined // within the test, which we can't track - if ( - !parent || - !isDirectlyWithinTestCaseCall(parent, context.getScope()) - ) { + if (!parent || !isDirectlyWithinTestCaseCall(parent, context)) { return; } diff --git a/src/rules/valid-title.ts b/src/rules/valid-title.ts index 88845aad3..ed55dcfee 100644 --- a/src/rules/valid-title.ts +++ b/src/rules/valid-title.ts @@ -179,9 +179,7 @@ export default createRule<[Options], MessageIds>({ return { CallExpression(node: TSESTree.CallExpression) { - const scope = context.getScope(); - - const jestFnCall = parseJestFnCall(node, scope); + const jestFnCall = parseJestFnCall(node, context); if (jestFnCall?.type !== 'describe' && jestFnCall?.type !== 'test') { return;