diff --git a/src/rules/__tests__/no-deprecated-functions.test.ts b/src/rules/__tests__/no-deprecated-functions.test.ts index eb2174e44..43ddeea8f 100644 --- a/src/rules/__tests__/no-deprecated-functions.test.ts +++ b/src/rules/__tests__/no-deprecated-functions.test.ts @@ -1,18 +1,18 @@ -import { TSESLint } from '@typescript-eslint/utils'; +import type { TSESLint } from '@typescript-eslint/utils'; import rule from '../no-deprecated-functions'; -import { - type JestVersion, - detectJestVersion, -} from '../utils/detectJestVersion'; -import { - FlatCompatRuleTester as RuleTester, - usingFlatConfig, -} from './test-utils'; - -jest.mock('../utils/detectJestVersion'); - -const detectJestVersionMock = detectJestVersion as jest.MockedFunction< - typeof detectJestVersion +import { type JestVersion, getJestVersion } from '../utils/detectJestVersion'; +import { FlatCompatRuleTester as RuleTester } from './test-utils'; + +jest.mock('../utils/detectJestVersion', () => { + const actual = jest.requireActual('../utils/detectJestVersion'); + + jest.spyOn(actual, 'getJestVersion'); + + return actual; +}); + +const getJestVersionMock = getJestVersion as jest.MockedFunction< + typeof getJestVersion >; const ruleTester = new RuleTester(); @@ -60,6 +60,31 @@ const generateInvalidCases = ( ]; }; +const generateNotDetectedCases = ( + deprecation: string, +): Array> => { + const [deprecatedName, deprecatedFunc] = deprecation.split('.'); + const settings = { jest: { version: undefined } }; + const errors: [TSESLint.TestCaseError<'jestNotDetected'>] = [ + { messageId: 'jestNotDetected' }, + ]; + + return [ + { + code: `${deprecation}()`, + output: null, + settings, + errors, + }, + { + code: `${deprecatedName}['${deprecatedFunc}']()`, + output: null, + settings, + errors, + }, + ]; +}; + // contains the cache-clearing beforeEach so we can test the cache too describe('the rule', () => { // a few sanity checks before doing our massive loop @@ -72,7 +97,7 @@ describe('the rule', () => { ...generateValidCases(25, 'jest.genMockFromModule'), ...generateValidCases('25.1.1', 'jest.genMockFromModule'), ...generateValidCases('17.2', 'require.requireActual'), - ], + ].filter(testCase => testCase.settings?.jest?.version), invalid: [ ...generateInvalidCases( 21, @@ -90,14 +115,14 @@ describe('the rule', () => { 'jest.genMockFromModule', 'jest.createMockFromModule', ), - ], + ].filter(testCase => testCase.settings?.jest?.version), }); describe.each([ 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, ])('when using jest version %i', jestVersion => { beforeEach(async () => { - detectJestVersionMock.mockReturnValue(jestVersion); + getJestVersionMock.mockReturnValue(jestVersion); }); const allowedFunctions: string[] = []; @@ -147,37 +172,21 @@ describe('the rule', () => { }); }); - describe('when there is an error in detecting the jest version', () => { - beforeEach(() => { - detectJestVersionMock.mockImplementation(() => { - throw new Error('oh noes!'); - }); + describe('when jest version not detected', () => { + beforeEach(async () => { + getJestVersionMock.mockReturnValue(null); }); - it('bubbles the error up', () => { - expect(() => { - const linter = new TSESLint.Linter(); - - /* istanbul ignore if */ - if (usingFlatConfig) { - linter.verify('jest.resetModuleRegistry()', [ - { - plugins: { - jest: { rules: { 'no-deprecated-functions': rule } }, - }, - rules: { 'jest/no-deprecated-functions': 'error' }, - }, - ]); - - return; - } - - linter.defineRule('no-deprecated-functions', rule); - - linter.verify('jest.resetModuleRegistry()', { - rules: { 'no-deprecated-functions': 'error' }, - }); - }).toThrow('oh noes!'); + ruleTester.run('no jest version', rule, { + valid: ['jest', 'require("fs")'], + invalid: [ + 'jest.resetModuleRegistry', + 'jest.addMatchers', + 'require.requireMock', + 'require.requireActual', + 'jest.runTimersToTime', + 'jest.genMockFromModule', + ].flatMap(generateNotDetectedCases), }); }); }); diff --git a/src/rules/no-deprecated-functions.ts b/src/rules/no-deprecated-functions.ts index d83323d47..66470e38b 100644 --- a/src/rules/no-deprecated-functions.ts +++ b/src/rules/no-deprecated-functions.ts @@ -1,29 +1,11 @@ import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; import { - type JestVersion, + type EslintPluginJestRuleContext, createRule, - detectJestVersion, + getJestVersion, getNodeName, } from './utils'; -interface ContextSettings { - jest?: EslintPluginJestSettings; -} - -interface EslintPluginJestSettings { - version: JestVersion | string; -} - -const parseJestVersion = (rawVersion: number | string): JestVersion => { - if (typeof rawVersion === 'number') { - return rawVersion; - } - - const [majorVersion] = rawVersion.split('.'); - - return parseInt(majorVersion, 10); -}; - export default createRule({ name: __filename, meta: { @@ -33,6 +15,8 @@ export default createRule({ messages: { deprecatedFunction: '`{{ deprecation }}` has been deprecated in favor of `{{ replacement }}`', + jestNotDetected: + 'Unable to detect Jest version - please ensure jest package is installed or set version explicitly', }, type: 'suggestion', schema: [], @@ -40,10 +24,11 @@ export default createRule({ }, defaultOptions: [], create(context) { - const jestVersion = parseJestVersion( - (context.settings as ContextSettings)?.jest?.version || - detectJestVersion(), - ); + // If jest version is not detected, it is set to Infinity so that all possible deprecations + // are reported with a "jest not detected" error message + const jestVersion = + getJestVersion(context as EslintPluginJestRuleContext) || Infinity; + const jestNotDetected = jestVersion === Infinity; const deprecations: Record = { ...(jestVersion >= 15 && { @@ -80,13 +65,16 @@ export default createRule({ const { callee } = node; context.report({ - messageId: 'deprecatedFunction', + messageId: jestNotDetected ? 'jestNotDetected' : 'deprecatedFunction', data: { deprecation, replacement, }, node, fix(fixer) { + if (jestNotDetected) { + return []; + } let [name, func] = replacement.split('.'); if (callee.property.type === AST_NODE_TYPES.Literal) { diff --git a/src/rules/utils/__tests__/detectJestVersion.test.ts b/src/rules/utils/__tests__/detectJestVersion.test.ts index 68fa16545..81a5d8b3a 100644 --- a/src/rules/utils/__tests__/detectJestVersion.test.ts +++ b/src/rules/utils/__tests__/detectJestVersion.test.ts @@ -93,12 +93,10 @@ describe('detectJestVersion', () => { }); describe('when the package.json is missing the version property', () => { - it('throws an error', () => { + it('returns null', () => { packageJsonFactory.mockReturnValue({}); - expect(() => detectJestVersion()).toThrow( - /Unable to detect Jest version/iu, - ); + expect(detectJestVersion()).toBeNull(); }); }); @@ -184,7 +182,7 @@ describe('detectJestVersion', () => { }); describe('when jest is not installed', () => { - it('throws an error', () => { + it('returns null', () => { const projectDir = setupFakeProject({ 'package.json': { name: 'no-jest' }, [`node_modules/${relativePathToFn}`]: compiledFn, @@ -193,8 +191,8 @@ describe('detectJestVersion', () => { const { stdout, stderr } = runDetectJestVersion(projectDir); - expect(stdout).toBe('undefined'); - expect(stderr).toContain('Unable to detect Jest version'); + expect(stdout).toBe('null'); + expect(stderr).toContain(''); }); }); diff --git a/src/rules/utils/detectJestVersion.ts b/src/rules/utils/detectJestVersion.ts index 7935d9a6c..5e04b7cba 100644 --- a/src/rules/utils/detectJestVersion.ts +++ b/src/rules/utils/detectJestVersion.ts @@ -1,4 +1,19 @@ import type { JSONSchemaForNPMPackageJsonFiles } from '@schemastore/package'; +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; + +interface ContextSettings { + [key: string]: unknown; + jest?: EslintPluginJestSettings; +} + +interface EslintPluginJestSettings { + version: JestVersion | string; +} + +export interface EslintPluginJestRuleContext + extends Readonly> { + settings: ContextSettings; +} export type JestVersion = | 14 @@ -22,7 +37,25 @@ export type JestVersion = let cachedJestVersion: JestVersion | null = null; -export const detectJestVersion = (): JestVersion => { +const parseJestVersion = (rawVersion: number | string): JestVersion => { + if (typeof rawVersion === 'number') { + return rawVersion; + } + + const [majorVersion] = rawVersion.split('.'); + + return parseInt(majorVersion, 10); +}; + +export const getContextJestVersion = ( + context: EslintPluginJestRuleContext, +): JestVersion | null => { + return context.settings.jest?.version + ? parseJestVersion(context.settings.jest.version) + : null; +}; + +export const detectJestVersion = (): JestVersion | null => { if (cachedJestVersion) { return cachedJestVersion; } @@ -35,13 +68,15 @@ export const detectJestVersion = (): JestVersion => { require(jestPath) as JSONSchemaForNPMPackageJsonFiles; if (jestPackageJson.version) { - const [majorVersion] = jestPackageJson.version.split('.'); - - return (cachedJestVersion = parseInt(majorVersion, 10)); + return (cachedJestVersion = parseJestVersion(jestPackageJson.version)); } } catch {} - throw new Error( - 'Unable to detect Jest version - please ensure jest package is installed, or otherwise set version explicitly', - ); + return null; +}; + +export const getJestVersion = ( + context: EslintPluginJestRuleContext, +): JestVersion | null => { + return getContextJestVersion(context) || detectJestVersion(); };