diff --git a/eslint.config.mjs b/eslint.config.mjs index cf16199c96ba..fa6080adb037 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -33,6 +33,8 @@ const vitestFiles = [ 'packages/eslint-plugin-internal/tests/**/*.test.{ts,tsx,cts,mts}', 'packages/typescript-eslint/tests/**/*.test.{ts,tsx,cts,mts}', 'packages/visitor-keys/tests/**/*.test.{ts,tsx,cts,mts}', + 'packages/parser/tests/lib/**/*.test.{ts,tsx,cts,mts}', + 'packages/parser/tests/test-utils/**/*.{ts,tsx,cts,mts}', ]; export default tseslint.config( @@ -379,18 +381,13 @@ export default tseslint.config( // define the vitest globals for all test files { files: vitestFiles, - languageOptions: { - globals: { - ...vitestPlugin.environments.env.globals, - }, - }, + ...vitestPlugin.configs.env, }, // test file specific configuration { files: [ 'packages/*/tests/**/*.test.{ts,tsx,cts,mts}', 'packages/*/tests/**/test.{ts,tsx,cts,mts}', - 'packages/parser/tests/**/*.{ts,tsx,cts,mts}', 'packages/integration-tests/tools/integration-test-base.ts', 'packages/integration-tests/tools/pack-packages.ts', ], @@ -440,6 +437,7 @@ export default tseslint.config( 'vitest/no-identical-title': 'error', 'vitest/no-test-prefixes': 'error', 'vitest/no-test-return-statement': 'error', + 'vitest/prefer-describe-function-title': 'error', 'vitest/prefer-each': 'error', 'vitest/prefer-spy-on': 'error', 'vitest/prefer-to-be': 'error', diff --git a/knip.ts b/knip.ts index d3fbad90f9a8..e3bd837e2e7e 100644 --- a/knip.ts +++ b/knip.ts @@ -65,6 +65,11 @@ export default { }, 'packages/parser': { ignore: ['tests/fixtures/**'], + + vitest: { + config: ['vitest.config.mts'], + entry: ['tests/lib/**/*.{bench,test,test-d}.?(c|m)ts?(x)'], + }, }, 'packages/rule-tester': { ignore: ['typings/eslint.d.ts'], diff --git a/package.json b/package.json index f2a4c9ad6700..b41ff68c1177 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "@typescript-eslint/typescript-estree": "workspace:^", "@typescript-eslint/utils": "workspace:^", "@vitest/coverage-v8": "^3.1.1", - "@vitest/eslint-plugin": "^1.1.39", + "@vitest/eslint-plugin": "^1.1.42", "console-fail-test": "^0.5.0", "cross-fetch": "^4.0.0", "cspell": "^8.15.2", @@ -126,7 +126,7 @@ "tsx": "*", "typescript": ">=4.8.4 <5.9.0", "typescript-eslint": "workspace:^", - "vite": "^6.2.5", + "vite": "^6.2.6", "vitest": "^3.1.1", "yargs": "17.7.2" }, diff --git a/packages/parser/jest.config.js b/packages/parser/jest.config.js deleted file mode 100644 index 7dd5748f98bb..000000000000 --- a/packages/parser/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -// @ts-check -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - ...require('../../jest.config.base.js'), - testRegex: './tests/lib/.+\\.test\\.ts$', -}; diff --git a/packages/parser/package.json b/packages/parser/package.json index 9310828dd44a..4e1b843e7c0b 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -43,10 +43,10 @@ "build": "tsc -b tsconfig.build.json", "postbuild": "downlevel-dts dist _ts4.3/dist --to=4.3", "clean": "tsc -b tsconfig.build.json --clean", - "postclean": "rimraf dist && rimraf _ts4.3 && rimraf coverage", + "postclean": "rimraf dist/ _ts4.3/ coverage/", "format": "prettier --write \"./**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md,css}\" --ignore-path ../../.prettierignore", "lint": "npx nx lint", - "test": "jest", + "test": "vitest --run --config=$INIT_CWD/vitest.config.mts", "check-types": "npx nx typecheck" }, "peerDependencies": { @@ -61,13 +61,13 @@ "debug": "^4.3.4" }, "devDependencies": { - "@jest/types": "29.6.3", + "@vitest/coverage-v8": "^3.1.1", "downlevel-dts": "*", "glob": "*", - "jest": "29.7.0", "prettier": "^3.2.5", "rimraf": "*", - "typescript": "*" + "typescript": "*", + "vitest": "^3.1.1" }, "funding": { "type": "opencollective", diff --git a/packages/parser/project.json b/packages/parser/project.json index 94b5289ff17e..6b9e1efe5f09 100644 --- a/packages/parser/project.json +++ b/packages/parser/project.json @@ -1,12 +1,16 @@ { "name": "parser", "$schema": "../../node_modules/nx/schemas/project-schema.json", - "type": "library", - "implicitDependencies": [], + "projectType": "library", + "root": "packages/parser", + "sourceRoot": "packages/parser/src", "targets": { "lint": { "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vite:test" } } } diff --git a/packages/parser/tests/lib/__snapshots__/services.test.ts.snap b/packages/parser/tests/lib/__snapshots__/services.test.ts.snap index e9e804b60eba..640ac5aedb34 100644 --- a/packages/parser/tests/lib/__snapshots__/services.test.ts.snap +++ b/packages/parser/tests/lib/__snapshots__/services.test.ts.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`services fixtures/isolated-file.src 1`] = ` +exports[`services > fixtures/isolated-file.src 1`] = ` { "body": [ { diff --git a/packages/parser/tests/lib/parser.test.ts b/packages/parser/tests/lib/parser.test.ts index 4da882979b29..39c072a8d42e 100644 --- a/packages/parser/tests/lib/parser.test.ts +++ b/packages/parser/tests/lib/parser.test.ts @@ -9,7 +9,7 @@ import { parse, parseForESLint } from '../../src/parser'; describe('parser', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('parse() should return just the AST from parseForESLint()', () => { @@ -24,7 +24,7 @@ describe('parser', () => { it('parseAndGenerateServices() should be called with options', () => { const code = 'const valid = true;'; - const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices'); + const spy = vi.spyOn(typescriptESTree, 'parseAndGenerateServices'); const config: ParserOptions = { ecmaFeatures: { globalReturn: false, @@ -36,11 +36,10 @@ describe('parser', () => { extraFileExtensions: ['.foo'], filePath: './isolated-file.src.ts', project: 'tsconfig.json', - tsconfigRootDir: path.resolve(__dirname, '../fixtures/services'), + tsconfigRootDir: path.join(__dirname, '..', 'fixtures', 'services'), }; parseForESLint(code, config); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenLastCalledWith(code, { + expect(spy).toHaveBeenCalledExactlyOnceWith(code, { comment: true, jsx: false, loc: true, @@ -52,9 +51,9 @@ describe('parser', () => { it('overrides `errorOnTypeScriptSyntacticAndSemanticIssues: false` when provided `errorOnTypeScriptSyntacticAndSemanticIssues: false`', () => { const code = 'const valid = true;'; - const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices'); + const spy = vi.spyOn(typescriptESTree, 'parseAndGenerateServices'); parseForESLint(code, { errorOnTypeScriptSyntacticAndSemanticIssues: true }); - expect(spy).toHaveBeenCalledWith(code, { + expect(spy).toHaveBeenCalledExactlyOnceWith(code, { comment: true, ecmaFeatures: {}, errorOnTypeScriptSyntacticAndSemanticIssues: false, @@ -68,9 +67,9 @@ describe('parser', () => { it('sets `loggerFn: false` on typescript-estree when provided `warnOnUnsupportedTypeScriptVersion: false`', () => { const code = 'const valid = true;'; - const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices'); + const spy = vi.spyOn(typescriptESTree, 'parseAndGenerateServices'); parseForESLint(code, { warnOnUnsupportedTypeScriptVersion: false }); - expect(spy).toHaveBeenCalledWith(code, { + expect(spy).toHaveBeenCalledExactlyOnceWith(code, { comment: true, ecmaFeatures: {}, errorOnTypeScriptSyntacticAndSemanticIssues: false, @@ -86,9 +85,9 @@ describe('parser', () => { it('sets `loggerFn: false` on typescript-estree when provided `warnOnUnsupportedTypeScriptVersion: true`', () => { const code = 'const valid = true;'; - const spy = jest.spyOn(typescriptESTree, 'parseAndGenerateServices'); + const spy = vi.spyOn(typescriptESTree, 'parseAndGenerateServices'); parseForESLint(code, { warnOnUnsupportedTypeScriptVersion: true }); - expect(spy).toHaveBeenCalledWith(code, { + expect(spy).toHaveBeenCalledExactlyOnceWith(code, { comment: true, ecmaFeatures: {}, errorOnTypeScriptSyntacticAndSemanticIssues: false, @@ -103,18 +102,17 @@ describe('parser', () => { it('should call analyze() with inferred analyze options when no analyze options are provided', () => { const code = 'const valid = true;'; - const spy = jest.spyOn(scopeManager, 'analyze'); + const spy = vi.spyOn(scopeManager, 'analyze'); const config: ParserOptions = { errorOnTypeScriptSyntacticAndSemanticIssues: false, filePath: 'isolated-file.src.ts', project: 'tsconfig.json', - tsconfigRootDir: path.join(__dirname, '../fixtures/services'), + tsconfigRootDir: path.join(__dirname, '..', 'fixtures', 'services'), }; parseForESLint(code, config); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenLastCalledWith(expect.anything(), { + expect(spy).toHaveBeenCalledExactlyOnceWith(expect.anything(), { globalReturn: undefined, jsxFragmentName: undefined, jsxPragma: undefined, @@ -123,7 +121,7 @@ describe('parser', () => { }); }); - it.each([ + it.for([ ['esnext.full', ScriptTarget.ESNext], ['es2022.full', ScriptTarget.ES2022], ['es2021.full', ScriptTarget.ES2021], @@ -135,32 +133,32 @@ describe('parser', () => { ['es6', ScriptTarget.ES2015], ['lib', ScriptTarget.ES5], ['lib', undefined], - ])( + ] as const)( 'calls analyze() with `lib: [%s]` when the compiler options target is %s', - (lib, target) => { + ([lib, target], { expect }) => { const code = 'const valid = true;'; - const spy = jest.spyOn(scopeManager, 'analyze'); + const spy = vi.spyOn(scopeManager, 'analyze'); const config: ParserOptions = { filePath: 'isolated-file.src.ts', project: 'tsconfig.json', - tsconfigRootDir: path.join(__dirname, '../fixtures/services'), + tsconfigRootDir: path.join(__dirname, '..', 'fixtures', 'services'), }; - jest - .spyOn(typescriptESTree, 'parseAndGenerateServices') - .mockReturnValueOnce({ - ast: {}, - services: { - program: { - getCompilerOptions: () => ({ target }), - }, + vi.spyOn( + typescriptESTree, + 'parseAndGenerateServices', + ).mockReturnValueOnce({ + ast: {}, + services: { + program: { + getCompilerOptions: () => ({ target }), }, - } as typescriptESTree.ParseAndGenerateServicesResult); + }, + } as typescriptESTree.ParseAndGenerateServicesResult); parseForESLint(code, config); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenLastCalledWith( + expect(spy).toHaveBeenCalledExactlyOnceWith( expect.anything(), expect.objectContaining({ lib: [lib], @@ -171,7 +169,7 @@ describe('parser', () => { it('calls analyze() with the provided analyze options when analyze options are provided', () => { const code = 'const valid = true;'; - const spy = jest.spyOn(scopeManager, 'analyze'); + const spy = vi.spyOn(scopeManager, 'analyze'); const config: ParserOptions = { ecmaFeatures: { globalReturn: false, @@ -187,13 +185,12 @@ describe('parser', () => { extraFileExtensions: ['.foo'], filePath: 'isolated-file.src.ts', project: 'tsconfig.json', - tsconfigRootDir: path.join(__dirname, '../fixtures/services'), + tsconfigRootDir: path.join(__dirname, '..', 'fixtures', 'services'), }; parseForESLint(code, config); - expect(spy).toHaveBeenCalledTimes(1); - expect(spy).toHaveBeenLastCalledWith(expect.anything(), { + expect(spy).toHaveBeenCalledExactlyOnceWith(expect.anything(), { globalReturn: false, jsxFragmentName: 'Bar', jsxPragma: 'Foo', diff --git a/packages/parser/tests/lib/services.test.ts b/packages/parser/tests/lib/services.test.ts index 3b3121017610..3719c7127e82 100644 --- a/packages/parser/tests/lib/services.test.ts +++ b/packages/parser/tests/lib/services.test.ts @@ -1,6 +1,6 @@ import { createProgram } from '@typescript-eslint/typescript-estree'; import * as glob from 'glob'; -import fs from 'node:fs'; +import fs from 'node:fs/promises'; import path from 'node:path'; import type { ParserOptions } from '../../src/parser'; @@ -15,7 +15,14 @@ import { // Setup //------------------------------------------------------------------------------ -const FIXTURES_DIR = './tests/fixtures/services'; +const FIXTURES_DIR = path.join( + __dirname, + '..', + '..', + 'tests', + 'fixtures', + 'services', +); const testFiles = glob.sync(`**/*.src.ts`, { cwd: FIXTURES_DIR, }); @@ -32,18 +39,19 @@ function createConfig(filename: string): ParserOptions { // Tests //------------------------------------------------------------------------------ -describe('services', () => { - const program = createProgram(path.resolve(FIXTURES_DIR, 'tsconfig.json')); - testFiles.forEach(filename => { - const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); - const config = createConfig(filename); - const snapshotName = formatSnapshotName(filename, FIXTURES_DIR, '.ts'); - it(snapshotName, createSnapshotTestBlock(code, config)); - it(`${snapshotName} services`, () => { - testServices(code, config); - }); - it(`${snapshotName} services with provided program`, () => { - testServices(code, { ...config, program }); - }); +const program = createProgram(path.resolve(FIXTURES_DIR, 'tsconfig.json')); + +describe.for(testFiles)('services', async filename => { + const code = await fs.readFile(path.join(FIXTURES_DIR, filename), { + encoding: 'utf-8', + }); + const config = createConfig(filename); + const snapshotName = formatSnapshotName(filename, FIXTURES_DIR, '.ts'); + it(snapshotName, createSnapshotTestBlock(code, config)); + it(`${snapshotName} services`, () => { + testServices(code, config); + }); + it(`${snapshotName} services with provided program`, () => { + testServices(code, { ...config, program }); }); }); diff --git a/packages/parser/tests/lib/tsx.test.ts b/packages/parser/tests/lib/tsx.test.ts index 5b6922e2c8f3..f58a859f41f7 100644 --- a/packages/parser/tests/lib/tsx.test.ts +++ b/packages/parser/tests/lib/tsx.test.ts @@ -1,5 +1,3 @@ -import type { ParserOptions } from '@typescript-eslint/types'; - import { parseForESLint } from '../../src/parser'; import { serializer } from '../test-utils/ts-error-serializer'; @@ -9,20 +7,12 @@ import { serializer } from '../test-utils/ts-error-serializer'; expect.addSnapshotSerializer(serializer); -function parseWithError(code: string, options?: ParserOptions | null): unknown { - try { - return parseForESLint(code, options); - } catch (e) { - return e; - } -} - describe('TSX', () => { describe("if the filename ends with '.tsx', enable jsx option automatically.", () => { it('filePath was not provided', () => { const code = 'const element = '; - expect(parseWithError(code)).toMatchInlineSnapshot(` + expect(() => parseForESLint(code)).toThrowErrorMatchingInlineSnapshot(` TSError { "column": 18, "index": 18, @@ -45,11 +35,11 @@ describe('TSX', () => { it('test.ts', () => { const code = 'const element = '; - expect( - parseWithError(code, { + expect(() => + parseForESLint(code, { filePath: 'test.ts', }), - ).toMatchInlineSnapshot(` + ).toThrowErrorMatchingInlineSnapshot(` TSError { "column": 18, "index": 18, @@ -62,14 +52,14 @@ describe('TSX', () => { it("test.ts with 'jsx:true' option", () => { const code = 'const element = '; - expect( - parseWithError(code, { + expect(() => + parseForESLint(code, { ecmaFeatures: { jsx: true, }, filePath: 'test.ts', }), - ).toMatchInlineSnapshot(` + ).toThrowErrorMatchingInlineSnapshot(` TSError { "column": 18, "index": 18, diff --git a/packages/parser/tsconfig.build.json b/packages/parser/tsconfig.build.json index dbcb0b0ac6c0..72d5737a808c 100644 --- a/packages/parser/tsconfig.build.json +++ b/packages/parser/tsconfig.build.json @@ -9,7 +9,7 @@ "types": ["node"] }, "include": ["src/**/*.ts", "typings"], - "exclude": ["jest.config.js", "src/**/*.spec.ts", "src/**/*.test.ts"], + "exclude": ["vitest.config.mts", "src/**/*.spec.ts", "src/**/*.test.ts"], "references": [ { "path": "../visitor-keys/tsconfig.build.json" diff --git a/packages/parser/tsconfig.spec.json b/packages/parser/tsconfig.spec.json index e3efdafbc520..be15ae29f809 100644 --- a/packages/parser/tsconfig.spec.json +++ b/packages/parser/tsconfig.spec.json @@ -3,10 +3,12 @@ "compilerOptions": { "outDir": "../../dist/out-tsc/packages/parser", "module": "NodeNext", - "types": ["jest", "node"] + "resolveJsonModule": true, + "types": ["node", "vitest/globals", "vitest/importMeta"] }, "include": [ - "jest.config.js", + "vitest.config.mts", + "package.json", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts", @@ -16,6 +18,9 @@ "references": [ { "path": "./tsconfig.build.json" + }, + { + "path": "../../tsconfig.spec.json" } ] } diff --git a/packages/parser/vitest.config.mts b/packages/parser/vitest.config.mts new file mode 100644 index 000000000000..be96a32226e6 --- /dev/null +++ b/packages/parser/vitest.config.mts @@ -0,0 +1,21 @@ +import * as path from 'node:path'; +import { defineProject, mergeConfig } from 'vitest/config'; + +import { vitestBaseConfig } from '../../vitest.config.base.mjs'; +import packageJson from './package.json' with { type: 'json' }; + +const vitestConfig = mergeConfig( + vitestBaseConfig, + + defineProject({ + root: import.meta.dirname, + + test: { + dir: path.join(import.meta.dirname, 'tests', 'lib'), + name: packageJson.name.replace('@typescript-eslint/', ''), + root: import.meta.dirname, + }, + }), +); + +export default vitestConfig; diff --git a/yarn.lock b/yarn.lock index 05d7f2fb704d..6d266a2aac3c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6130,18 +6130,18 @@ __metadata: version: 0.0.0-use.local resolution: "@typescript-eslint/parser@workspace:packages/parser" dependencies: - "@jest/types": 29.6.3 "@typescript-eslint/scope-manager": 8.29.1 "@typescript-eslint/types": 8.29.1 "@typescript-eslint/typescript-estree": 8.29.1 "@typescript-eslint/visitor-keys": 8.29.1 + "@vitest/coverage-v8": ^3.1.1 debug: ^4.3.4 downlevel-dts: "*" glob: "*" - jest: 29.7.0 prettier: ^3.2.5 rimraf: "*" typescript: "*" + vitest: ^3.1.1 peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <5.9.0" @@ -6282,7 +6282,7 @@ __metadata: "@typescript-eslint/typescript-estree": "workspace:^" "@typescript-eslint/utils": "workspace:^" "@vitest/coverage-v8": ^3.1.1 - "@vitest/eslint-plugin": ^1.1.39 + "@vitest/eslint-plugin": ^1.1.42 console-fail-test: ^0.5.0 cross-fetch: ^4.0.0 cspell: ^8.15.2 @@ -6318,7 +6318,7 @@ __metadata: tsx: "*" typescript: ">=4.8.4 <5.9.0" typescript-eslint: "workspace:^" - vite: ^6.2.5 + vite: ^6.2.6 vitest: ^3.1.1 yargs: 17.7.2 languageName: unknown @@ -6446,20 +6446,18 @@ __metadata: languageName: node linkType: hard -"@vitest/eslint-plugin@npm:^1.1.39": - version: 1.1.39 - resolution: "@vitest/eslint-plugin@npm:1.1.39" +"@vitest/eslint-plugin@npm:^1.1.42": + version: 1.1.42 + resolution: "@vitest/eslint-plugin@npm:1.1.42" peerDependencies: - "@typescript-eslint/utils": ^8.24.0 + "@typescript-eslint/utils": ">= 8.24.0" eslint: ">= 8.57.0" typescript: ">= 5.0.0" vitest: "*" peerDependenciesMeta: typescript: optional: true - vitest: - optional: true - checksum: 0730c7d2a24b6e72ad74478991f7426fe889d0a326f4dc5034db5bfb1fcedeb1f54f8d90d61587e66c447139fa5c72af07003d740753860de82a9b0565bd14aa + checksum: 0b78745fde3cd6c35ce2cecd097133a41ff0c3787f9827538be1196e54b22d41a63c8ce2ac4571f29132399e96d6cf5ee47dcaf8a8f618bed8de80ad86851046 languageName: node linkType: hard @@ -19431,9 +19429,9 @@ __metadata: linkType: hard "std-env@npm:^3.8.1": - version: 3.8.1 - resolution: "std-env@npm:3.8.1" - checksum: 20114a5270aa2a3fc50d897461c6ab73329cf2d3c6bff1c124bb969577493aeebda8ee1916588b2657afcee9881bc652437cfdec6360e3f30be36c8675ea0cbb + version: 3.9.0 + resolution: "std-env@npm:3.9.0" + checksum: d40126e4a650f6e5456711e6c297420352a376ef99a9599e8224d2d8f2ff2b91a954f3264fcef888d94fce5c9ae14992c5569761c95556fc87248ce4602ed212 languageName: node linkType: hard @@ -20895,9 +20893,9 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.2.5": - version: 6.2.5 - resolution: "vite@npm:6.2.5" +"vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.2.6": + version: 6.2.6 + resolution: "vite@npm:6.2.6" dependencies: esbuild: ^0.25.0 fsevents: ~2.3.3 @@ -20943,7 +20941,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 49a6529c5ae8d6e4926f2daa51d7e20c50d780d8d2ec8c08605e966983fe8d17ec69bc36a356c1a21141c5a630b7a4109f3690c5b33f579d3e2bf26f914a149d + checksum: ddeb36d29c053c6d6f0e70eb01939848db611135878d85e9497fc4b899667f58ce35ea4014acf01342ee1cf115879280fac809c0a806ad6432833cde87fe90dc languageName: node linkType: hard