diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98af2e8ed085..355422cdcc8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -193,6 +193,38 @@ jobs: # Sadly 1 day is the minimum retention-days: 1 + unit_tests_tsserver: + name: Run Unit Tests with Experimental TSServer + needs: [build] + runs-on: ubuntu-latest + strategy: + matrix: + package: + [ + 'eslint-plugin', + 'eslint-plugin-internal', + 'eslint-plugin-tslint', + 'typescript-estree', + ] + env: + COLLECT_COVERAGE: false + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + - name: Install + uses: ./.github/actions/prepare-install + with: + node-version: 18 + - name: Build + uses: ./.github/actions/prepare-build + - name: Run unit tests for ${{ matrix.package }} + run: npx nx test ${{ matrix.package }} --coverage=false + env: + CI: true + TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER: true + website_tests: # The NETLIFY_TOKEN secret will not be available on forks if: github.repository_owner == 'typescript-eslint' diff --git a/.vscode/launch.json b/.vscode/launch.json index 4cee04bec4a0..82c02f90b2e5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -141,6 +141,40 @@ "${workspaceFolder}/packages/scope-manager/dist/index.js", ], }, + { + "type": "node", + "request": "launch", + "name": "Jest Test Current eslint-plugin-tslint Rule", + "cwd": "${workspaceFolder}/packages/eslint-plugin-tslint/", + "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", + "args": [ + "--runInBand", + "--no-cache", + "--no-coverage", + "${fileBasenameNoExtension}" + ], + "sourceMaps": true, + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "skipFiles": [ + "${workspaceFolder}/packages/utils/src/index.ts", + "${workspaceFolder}/packages/utils/dist/index.js", + "${workspaceFolder}/packages/utils/src/ts-estree.ts", + "${workspaceFolder}/packages/utils/dist/ts-estree.js", + "${workspaceFolder}/packages/type-utils/src/index.ts", + "${workspaceFolder}/packages/type-utils/dist/index.js", + "${workspaceFolder}/packages/parser/src/index.ts", + "${workspaceFolder}/packages/parser/dist/index.js", + "${workspaceFolder}/packages/typescript-estree/src/index.ts", + "${workspaceFolder}/packages/typescript-estree/dist/index.js", + "${workspaceFolder}/packages/types/src/index.ts", + "${workspaceFolder}/packages/types/dist/index.js", + "${workspaceFolder}/packages/visitor-keys/src/index.ts", + "${workspaceFolder}/packages/visitor-keys/dist/index.js", + "${workspaceFolder}/packages/scope-manager/dist/index.js", + "${workspaceFolder}/packages/scope-manager/dist/index.js", + ], + }, { "type": "node", "request": "launch", diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index 68e2706ca9a1..6354565e8282 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -147,6 +147,15 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + /** + * ***EXPERIMENTAL FLAG*** - Use this at your own risk. + * + * Whether to create a shared TypeScript server to power program creation. + * + * @see https://github.com/typescript-eslint/typescript-eslint/issues/6575 + */ + EXPERIMENTAL_useProjectService?: boolean; + /** * ***EXPERIMENTAL FLAG*** - Use this at your own risk. * @@ -155,7 +164,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { * * This flag REQUIRES at least TS v3.9, otherwise it does nothing. * - * See: https://github.com/typescript-eslint/typescript-eslint/issues/2094 + * @see https://github.com/typescript-eslint/typescript-eslint/issues/2094 */ EXPERIMENTAL_useSourceOfProjectReferenceRedirect?: boolean; diff --git a/packages/eslint-plugin-tslint/src/rules/config.ts b/packages/eslint-plugin-tslint/src/rules/config.ts index e7918218a905..23b7558b80fb 100644 --- a/packages/eslint-plugin-tslint/src/rules/config.ts +++ b/packages/eslint-plugin-tslint/src/rules/config.ts @@ -1,4 +1,5 @@ import { ESLintUtils } from '@typescript-eslint/utils'; +import path from 'path'; import type { RuleSeverity } from 'tslint'; import { Configuration } from 'tslint'; @@ -118,7 +119,7 @@ export default createRule({ context, [{ rules: tslintRules, rulesDirectory: tslintRulesDirectory, lintFile }], ) { - const fileName = context.getFilename(); + const fileName = path.resolve(context.getCwd(), context.getFilename()); const sourceCode = context.getSourceCode().text; const services = ESLintUtils.getParserServices(context); const program = services.program; diff --git a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts index 3c94e99ff141..1910b1986eeb 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts @@ -16,6 +16,7 @@ const ruleTester = new RuleTester({ }); const withMetaParserOptions = { + EXPERIMENTAL_useProjectService: false, tsconfigRootDir: getFixturesRootDir(), project: './tsconfig-withmeta.json', }; diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts index 397d4b507ccb..c68c735ae11e 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-condition.test.ts @@ -595,6 +595,7 @@ function getElem(dict: Record, key: string) { } `, parserOptions: { + EXPERIMENTAL_useProjectService: false, tsconfigRootDir: getFixturesRootDir(), project: './tsconfig.noUncheckedIndexedAccess.json', }, diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts index 27028234b316..fb2d713e647f 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-assignment.test.ts @@ -65,6 +65,7 @@ function assignmentTest( const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { + EXPERIMENTAL_useProjectService: false, project: './tsconfig.noImplicitThis.json', tsconfigRootDir: getFixturesRootDir(), }, diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts index b91b2ec6273d..bb844011fd7b 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-call.test.ts @@ -6,6 +6,7 @@ import { getFixturesRootDir } from '../RuleTester'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { + EXPERIMENTAL_useProjectService: false, project: './tsconfig.noImplicitThis.json', tsconfigRootDir: getFixturesRootDir(), }, diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-member-access.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-member-access.test.ts index b66b96d4663c..8298cec6ceba 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-member-access.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-member-access.test.ts @@ -6,6 +6,7 @@ import { getFixturesRootDir } from '../RuleTester'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { + EXPERIMENTAL_useProjectService: false, project: './tsconfig.noImplicitThis.json', tsconfigRootDir: getFixturesRootDir(), }, diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts index f0cbd8b25352..b75cdfb28c05 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-return.test.ts @@ -6,6 +6,7 @@ import { getFixturesRootDir } from '../RuleTester'; const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', parserOptions: { + EXPERIMENTAL_useProjectService: false, project: './tsconfig.noImplicitThis.json', tsconfigRootDir: getFixturesRootDir(), }, diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts index 2c651a1b61da..e891452136a2 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -14,6 +14,7 @@ const ruleTester = new RuleTester({ }); const withMetaParserOptions = { + EXPERIMENTAL_useProjectService: false, tsconfigRootDir: getFixturesRootDir(), project: './tsconfig-withmeta.json', }; diff --git a/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts b/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts index 2f5d7163de34..8b86634d2797 100644 --- a/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts +++ b/packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts @@ -204,6 +204,7 @@ const y = x!; const ruleTesterWithNoUncheckedIndexAccess = new RuleTester({ parserOptions: { + EXPERIMENTAL_useProjectService: false, sourceType: 'module', tsconfigRootDir: getFixturesRootDir(), project: './tsconfig.noUncheckedIndexedAccess.json', diff --git a/packages/parser/tests/lib/parser.ts b/packages/parser/tests/lib/parser.ts index 952e388eb8cf..e6bd731db075 100644 --- a/packages/parser/tests/lib/parser.ts +++ b/packages/parser/tests/lib/parser.ts @@ -1,6 +1,7 @@ import * as scopeManager from '@typescript-eslint/scope-manager'; import type { ParserOptions } from '@typescript-eslint/types'; import * as typescriptESTree from '@typescript-eslint/typescript-estree'; +import path from 'path'; import { parse, parseForESLint } from '../../src/parser'; @@ -33,10 +34,10 @@ describe('parser', () => { jsx: false, }, // ts-estree specific - filePath: 'isolated-file.src.ts', + filePath: './isolated-file.src.ts', project: 'tsconfig.json', errorOnTypeScriptSyntacticAndSemanticIssues: false, - tsconfigRootDir: 'tests/fixtures/services', + tsconfigRootDir: path.resolve(__dirname, '../fixtures/services'), extraFileExtensions: ['.foo'], }; parseForESLint(code, config); @@ -89,7 +90,7 @@ describe('parser', () => { filePath: 'isolated-file.src.ts', project: 'tsconfig.json', errorOnTypeScriptSyntacticAndSemanticIssues: false, - tsconfigRootDir: 'tests/fixtures/services', + tsconfigRootDir: path.join(__dirname, '../fixtures/services'), extraFileExtensions: ['.foo'], }; parseForESLint(code, config); diff --git a/packages/rule-tester/tests/RuleTester.test.ts b/packages/rule-tester/tests/RuleTester.test.ts index 93f9f6d35d24..25b6aa0888ed 100644 --- a/packages/rule-tester/tests/RuleTester.test.ts +++ b/packages/rule-tester/tests/RuleTester.test.ts @@ -169,6 +169,7 @@ describe('RuleTester', () => { { code: 'type-aware parser options should override the constructor config', parserOptions: { + EXPERIMENTAL_useProjectService: false, project: 'tsconfig.test-specific.json', tsconfigRootDir: '/set/in/the/test/', }, @@ -209,6 +210,7 @@ describe('RuleTester', () => { "code": "type-aware parser options should override the constructor config", "filename": "/set/in/the/test/file.ts", "parserOptions": { + "EXPERIMENTAL_useProjectService": false, "project": "tsconfig.test-specific.json", "tsconfigRootDir": "/set/in/the/test/", }, diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 8ddca54d3b34..a91358b4b172 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -196,18 +196,18 @@ describe('TypeOrValueSpecifier', () => { ], [ 'interface Foo {prop: string}; type Test = Foo;', - { from: 'file', name: 'Foo', path: 'tests/fixtures/file.ts' }, + { from: 'file', name: 'Foo', path: 'file.ts' }, ], [ 'type Foo = {prop: string}; type Test = Foo;', - { from: 'file', name: 'Foo', path: 'tests/fixtures/file.ts' }, + { from: 'file', name: 'Foo', path: 'file.ts' }, ], [ 'interface Foo {prop: string}; type Test = Foo;', { from: 'file', name: 'Foo', - path: 'tests/../tests/fixtures/////file.ts', + path: './////file.ts', }, ], [ @@ -215,7 +215,7 @@ describe('TypeOrValueSpecifier', () => { { from: 'file', name: 'Foo', - path: 'tests/../tests/fixtures/////file.ts', + path: './////file.ts', }, ], [ @@ -223,7 +223,7 @@ describe('TypeOrValueSpecifier', () => { { from: 'file', name: ['Foo', 'Bar'], - path: 'tests/fixtures/file.ts', + path: 'file.ts', }, ], [ @@ -231,7 +231,7 @@ describe('TypeOrValueSpecifier', () => { { from: 'file', name: ['Foo', 'Bar'], - path: 'tests/fixtures/file.ts', + path: 'file.ts', }, ], ])('matches a matching file specifier: %s', runTestPositive); @@ -247,14 +247,14 @@ describe('TypeOrValueSpecifier', () => { ], [ 'interface Foo {prop: string}; type Test = Foo;', - { from: 'file', name: 'Foo', path: 'tests/fixtures/wrong-file.ts' }, + { from: 'file', name: 'Foo', path: 'wrong-file.ts' }, ], [ 'interface Foo {prop: string}; type Test = Foo;', { from: 'file', name: ['Foo', 'Bar'], - path: 'tests/fixtures/wrong-file.ts', + path: 'wrong-file.ts', }, ], ])("doesn't match a mismatched file specifier: %s", runTestNegative); @@ -399,14 +399,14 @@ describe('TypeOrValueSpecifier', () => { ['type Test = RegExp;', { from: 'file', name: ['RegExp', 'BigInt'] }], [ 'type Test = RegExp;', - { from: 'file', name: 'RegExp', path: 'tests/fixtures/file.ts' }, + { from: 'file', name: 'RegExp', path: 'file.ts' }, ], [ 'type Test = RegExp;', { from: 'file', name: ['RegExp', 'BigInt'], - path: 'tests/fixtures/file.ts', + path: 'file.ts', }, ], [ diff --git a/packages/types/src/parser-options.ts b/packages/types/src/parser-options.ts index 4e8bb90dfae1..5389efba75eb 100644 --- a/packages/types/src/parser-options.ts +++ b/packages/types/src/parser-options.ts @@ -47,6 +47,7 @@ interface ParserOptions { debugLevel?: DebugLevel; errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; errorOnUnknownASTType?: boolean; + EXPERIMENTAL_useProjectService?: boolean; // purposely undocumented for now EXPERIMENTAL_useSourceOfProjectReferenceRedirect?: boolean; // purposely undocumented for now extraFileExtensions?: string[]; filePath?: string; diff --git a/packages/typescript-estree/src/clear-caches.ts b/packages/typescript-estree/src/clear-caches.ts index aea4d6cf845c..015fd18e29c8 100644 --- a/packages/typescript-estree/src/clear-caches.ts +++ b/packages/typescript-estree/src/clear-caches.ts @@ -1,6 +1,9 @@ import { clearWatchCaches } from './create-program/getWatchProgramsForProjects'; import { clearProgramCache as clearProgramCacheOriginal } from './parser'; -import { clearTSConfigMatchCache } from './parseSettings/createParseSettings'; +import { + clearTSConfigMatchCache, + clearTSServerProjectService, +} from './parseSettings/createParseSettings'; import { clearGlobCache } from './parseSettings/resolveProjectList'; /** @@ -14,6 +17,7 @@ export function clearCaches(): void { clearProgramCacheOriginal(); clearWatchCaches(); clearTSConfigMatchCache(); + clearTSServerProjectService(); clearGlobCache(); } diff --git a/packages/typescript-estree/src/create-program/createProjectProgram.ts b/packages/typescript-estree/src/create-program/createProjectProgram.ts index 51a2ebdfdfc6..edfe00992c19 100644 --- a/packages/typescript-estree/src/create-program/createProjectProgram.ts +++ b/packages/typescript-estree/src/create-program/createProjectProgram.ts @@ -5,7 +5,6 @@ import * as ts from 'typescript'; import { firstDefined } from '../node-utils'; import type { ParseSettings } from '../parseSettings'; import { describeFilePath } from './describeFilePath'; -import { getWatchProgramsForProjects } from './getWatchProgramsForProjects'; import type { ASTAndDefiniteProgram } from './shared'; import { getAstFromProgram } from './shared'; @@ -28,12 +27,12 @@ const DEFAULT_EXTRA_FILE_EXTENSIONS = [ */ function createProjectProgram( parseSettings: ParseSettings, + programsForProjects: readonly ts.Program[], ): ASTAndDefiniteProgram | undefined { log('Creating project program for: %s', parseSettings.filePath); - const programsForProjects = getWatchProgramsForProjects(parseSettings); const astAndProgram = firstDefined(programsForProjects, currentProgram => - getAstFromProgram(currentProgram, parseSettings), + getAstFromProgram(currentProgram, parseSettings.filePath), ); // The file was either matched within the tsconfig, or we allow creating a default program diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts new file mode 100644 index 000000000000..333d221f85b1 --- /dev/null +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -0,0 +1,49 @@ +/* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ +import type * as ts from 'typescript/lib/tsserverlibrary'; + +const doNothing = (): void => {}; + +const createStubFileWatcher = (): ts.FileWatcher => ({ + close: doNothing, +}); + +export type TypeScriptProjectService = ts.server.ProjectService; + +export function createProjectService(): TypeScriptProjectService { + // We import this lazily to avoid its cost for users who don't use the service + const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts; + + // TODO: see getWatchProgramsForProjects + // We don't watch the disk, we just refer to these when ESLint calls us + // there's a whole separate update pass in maybeInvalidateProgram at the bottom of getWatchProgramsForProjects + // (this "goes nuclear on TypeScript") + const system: ts.server.ServerHost = { + ...tsserver.sys, + clearImmediate, + clearTimeout, + setImmediate, + setTimeout, + watchDirectory: createStubFileWatcher, + watchFile: createStubFileWatcher, + }; + + return new tsserver.server.ProjectService({ + host: system, + cancellationToken: { isCancellationRequested: (): boolean => false }, + useSingleInferredProject: false, + useInferredProjectPerProjectRoot: false, + logger: { + close: doNothing, + endGroup: doNothing, + getLogFileName: () => undefined, + hasLevel: () => false, + info: doNothing, + loggingEnabled: () => false, + msg: doNothing, + perftrc: doNothing, + startGroup: doNothing, + }, + session: undefined, + }); +} +/* eslint-enable @typescript-eslint/no-empty-function */ diff --git a/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts b/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts index 2ec2b4ce35ae..e5e4b70f7d0a 100644 --- a/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts +++ b/packages/typescript-estree/src/create-program/getWatchProgramsForProjects.ts @@ -261,7 +261,10 @@ function createWatchProgram( const watchCompilerHost = ts.createWatchCompilerHost( tsconfigPath, createDefaultCompilerOptionsFromExtra(parseSettings), - ts.sys, + { + ...ts.sys, + getCurrentDirectory: () => parseSettings.tsconfigRootDir, + }, ts.createAbstractBuilder, diagnosticReporter, // TODO: file issue on TypeScript to suggest making optional? diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index b0e39d19e7e6..8966093372ed 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -97,12 +97,12 @@ function getExtension(fileName: string | undefined): string | null { function getAstFromProgram( currentProgram: Program, - parseSettings: ParseSettings, + filePath: string, ): ASTAndDefiniteProgram | undefined { - const ast = currentProgram.getSourceFile(parseSettings.filePath); + const ast = currentProgram.getSourceFile(filePath); // working around https://github.com/typescript-eslint/typescript-eslint/issues/1573 - const expectedExt = getExtension(parseSettings.filePath); + const expectedExt = getExtension(filePath); const returnedExt = getExtension(ast?.fileName); if (expectedExt !== returnedExt) { return undefined; diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index 96093e9a3afa..c2b67e795750 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -3,24 +3,25 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import type { ParseSettings } from '../parseSettings'; import type { ASTAndDefiniteProgram } from './shared'; import { CORE_COMPILER_OPTIONS, getAstFromProgram } from './shared'; const log = debug('typescript-eslint:typescript-estree:useProvidedProgram'); +export interface ProvidedProgramsSettings { + filePath: string; + tsconfigRootDir: string; +} + function useProvidedPrograms( programInstances: Iterable, - parseSettings: ParseSettings, + { filePath, tsconfigRootDir }: ProvidedProgramsSettings, ): ASTAndDefiniteProgram | undefined { - log( - 'Retrieving ast for %s from provided program instance(s)', - parseSettings.filePath, - ); + log('Retrieving ast for %s from provided program instance(s)', filePath); let astAndProgram: ASTAndDefiniteProgram | undefined; for (const programInstance of programInstances) { - astAndProgram = getAstFromProgram(programInstance, parseSettings); + astAndProgram = getAstFromProgram(programInstance, filePath); // Stop at the first applicable program instance if (astAndProgram) { break; @@ -29,8 +30,8 @@ function useProvidedPrograms( if (!astAndProgram) { const relativeFilePath = path.relative( - parseSettings.tsconfigRootDir || process.cwd(), - parseSettings.filePath, + tsconfigRootDir || process.cwd(), + filePath, ); const errorLines = [ '"parserOptions.programs" has been provided for @typescript-eslint/parser.', diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index b26e00376977..3062a6164b32 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,6 +1,8 @@ import debug from 'debug'; import type * as ts from 'typescript'; +import type { TypeScriptProjectService } from '../create-program/createProjectService'; +import { createProjectService } from '../create-program/createProjectService'; import { ensureAbsolutePath } from '../create-program/shared'; import type { TSESTreeOptions } from '../parser-options'; import { isSourceFile } from '../source-files'; @@ -19,6 +21,7 @@ const log = debug( ); let TSCONFIG_MATCH_CACHE: ExpiringCache | null; +let TSSERVER_PROJECT_SERVICE: TypeScriptProjectService | null = null; export function createParseSettings( code: ts.SourceFile | string, @@ -47,6 +50,13 @@ export function createParseSettings( : new Set(), errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: options.errorOnUnknownASTType === true, + EXPERIMENTAL_projectService: + (options.EXPERIMENTAL_useProjectService === true && + process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'false') || + (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' && + options.EXPERIMENTAL_useProjectService !== false) + ? (TSSERVER_PROJECT_SERVICE ??= createProjectService()) + : undefined, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, extraFileExtensions: @@ -114,8 +124,8 @@ export function createParseSettings( ); } - // Providing a program overrides project resolution - if (!parseSettings.programs) { + // Providing a program or project service overrides project resolution + if (!parseSettings.programs && !parseSettings.EXPERIMENTAL_projectService) { parseSettings.projects = resolveProjectList({ cacheLifetime: options.cacheLifetime, project: getProjectConfigFiles(parseSettings, options.project), @@ -134,6 +144,10 @@ export function clearTSConfigMatchCache(): void { TSCONFIG_MATCH_CACHE?.clear(); } +export function clearTSServerProjectService(): void { + TSSERVER_PROJECT_SERVICE = null; +} + /** * Ensures source code is a string. */ diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 3cf3e2a6f692..da093dedfed9 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -1,4 +1,5 @@ import type * as ts from 'typescript'; +import type * as tsserverlibrary from 'typescript/lib/tsserverlibrary'; import type { CanonicalPath } from '../create-program/shared'; import type { TSESTree } from '../ts-estree'; @@ -57,6 +58,13 @@ export interface MutableParseSettings { */ errorOnUnknownASTType: boolean; + /** + * Experimental: TypeScript server to power program creation. + */ + EXPERIMENTAL_projectService: + | tsserverlibrary.server.ProjectService + | undefined; + /** * Whether TS should use the source files for referenced projects instead of the compiled .d.ts files. * diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index b867f32e63b7..711ef4bca34b 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -94,6 +94,15 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ errorOnTypeScriptSyntacticAndSemanticIssues?: boolean; + /** + * ***EXPERIMENTAL FLAG*** - Use this at your own risk. + * + * Whether to create a shared TypeScript server to power program creation. + * + * @see https://github.com/typescript-eslint/typescript-eslint/issues/6575 + */ + EXPERIMENTAL_useProjectService?: boolean; + /** * ***EXPERIMENTAL FLAG*** - Use this at your own risk. * @@ -102,7 +111,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { * * This flag REQUIRES at least TS v3.9, otherwise it does nothing. * - * See: https://github.com/typescript-eslint/typescript-eslint/issues/2094 + * @see https://github.com/typescript-eslint/typescript-eslint/issues/2094 */ EXPERIMENTAL_useSourceOfProjectReferenceRedirect?: boolean; diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 63723c6dd628..4bf5dec26bd9 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -10,6 +10,7 @@ import { createNoProgram, createSourceFile, } from './create-program/createSourceFile'; +import { getWatchProgramsForProjects } from './create-program/getWatchProgramsForProjects'; import type { ASTAndProgram, CanonicalPath } from './create-program/shared'; import { createProgramFromConfigFile, @@ -25,6 +26,7 @@ import type { ParseSettings } from './parseSettings'; import { createParseSettings } from './parseSettings/createParseSettings'; import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import type { TSESTree } from './ts-estree'; +import { useProgramFromProjectService } from './useProgramFromProjectService'; const log = debug('typescript-eslint:typescript-estree:parser'); @@ -47,6 +49,16 @@ function getProgramAndAST( parseSettings: ParseSettings, hasFullTypeInformation: boolean, ): ASTAndProgram { + if (parseSettings.EXPERIMENTAL_projectService) { + const fromProjectService = useProgramFromProjectService( + parseSettings.EXPERIMENTAL_projectService, + parseSettings, + ); + if (fromProjectService) { + return fromProjectService; + } + } + if (parseSettings.programs) { const fromProvidedPrograms = useProvidedPrograms( parseSettings.programs, @@ -57,27 +69,30 @@ function getProgramAndAST( } } - if (hasFullTypeInformation) { - const fromProjectProgram = createProjectProgram(parseSettings); - if (fromProjectProgram) { - return fromProjectProgram; - } + // no need to waste time creating a program as the caller didn't want parser services + // so we can save time and just create a lonesome source file + if (!hasFullTypeInformation) { + return createNoProgram(parseSettings); + } + + const fromProjectProgram = createProjectProgram( + parseSettings, + getWatchProgramsForProjects(parseSettings), + ); + if (fromProjectProgram) { + return fromProjectProgram; + } + // eslint-disable-next-line deprecation/deprecation -- will be cleaned up with the next major + if (parseSettings.DEPRECATED__createDefaultProgram) { // eslint-disable-next-line deprecation/deprecation -- will be cleaned up with the next major - if (parseSettings.DEPRECATED__createDefaultProgram) { - // eslint-disable-next-line deprecation/deprecation -- will be cleaned up with the next major - const fromDefaultProgram = createDefaultProgram(parseSettings); - if (fromDefaultProgram) { - return fromDefaultProgram; - } + const fromDefaultProgram = createDefaultProgram(parseSettings); + if (fromDefaultProgram) { + return fromDefaultProgram; } - - return createIsolatedProgram(parseSettings); } - // no need to waste time creating a program as the caller didn't want parser services - // so we can save time and just create a lonesome source file - return createNoProgram(parseSettings); + return createIsolatedProgram(parseSettings); } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts new file mode 100644 index 000000000000..16a7933a671c --- /dev/null +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -0,0 +1,40 @@ +import path from 'path'; +import type * as ts from 'typescript'; +import type { server } from 'typescript/lib/tsserverlibrary'; + +import { createProjectProgram } from './create-program/createProjectProgram'; +import { type ASTAndDefiniteProgram } from './create-program/shared'; +import type { MutableParseSettings } from './parseSettings'; + +export function useProgramFromProjectService( + projectService: server.ProjectService, + parseSettings: Readonly, +): ASTAndDefiniteProgram | undefined { + const opened = projectService.openClientFile( + absolutify(parseSettings.filePath), + parseSettings.codeFullText, + /* scriptKind */ undefined, + parseSettings.tsconfigRootDir, + ); + if (!opened.configFileName) { + return undefined; + } + + const scriptInfo = projectService.getScriptInfo(parseSettings.filePath); + const program = projectService + .getDefaultProjectForFile(scriptInfo!.fileName, true)! + .getLanguageService(/*ensureSynchronized*/ true) + .getProgram(); + + if (!program) { + return undefined; + } + + return createProjectProgram(parseSettings, [program as ts.Program]); + + function absolutify(filePath: string): string { + return path.isAbsolute(filePath) + ? filePath + : path.join(projectService.host.getCurrentDirectory(), filePath); + } +} diff --git a/packages/typescript-estree/tests/lib/__snapshots__/parse.test.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/parse.test.ts.snap index 0f777af2df28..c4182c42113a 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/parse.test.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/parse.test.ts.snap @@ -1,87 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is empty the extension does not match 1`] = ` -"ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json -The extension for the file (\`.unknown\`) is non-standard. You should add \`parserOptions.extraFileExtensions\` to your config." -`; - -exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is non-empty invalid extension 1`] = ` -"ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json -Found unexpected extension \`unknown\` specified with the \`parserOptions.extraFileExtensions\` option. Did you mean \`.unknown\`? -The extension for the file (\`.unknown\`) is non-standard. It should be added to your existing \`parserOptions.extraFileExtensions\`." -`; - -exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension does not match 1`] = ` -"ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json -The extension for the file (\`.unknown\`) is non-standard. It should be added to your existing \`parserOptions.extraFileExtensions\`." -`; - -exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension matches duplicate extension 1`] = ` -"ESLint was configured to run on \`/ts/notIncluded.ts\` using \`parserOptions.project\`: /tsconfig.json -You unnecessarily included the extension \`.ts\` with the \`parserOptions.extraFileExtensions\` option. This extension is already handled by the parser by default. -However, that TSConfig does not include this file. Either: -- Change ESLint's list of included files to not include this file -- Change that TSConfig to include this file -- Create a new TSConfig that includes this file and include it in your parserOptions.project -See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" -`; - -exports[`parseAndGenerateServices invalid file error messages "parserOptions.extraFileExtensions" is non-empty the extension matches the file isn't included 1`] = ` -"ESLint was configured to run on \`/other/notIncluded.vue\` using \`parserOptions.project\`: /tsconfig.json -However, that TSConfig does not include this file. Either: -- Change ESLint's list of included files to not include this file -- Change that TSConfig to include this file -- Create a new TSConfig that includes this file and include it in your parserOptions.project -See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" -`; - -exports[`parseAndGenerateServices invalid file error messages project includes errors for not included files 1`] = ` -"ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: /tsconfig.json -However, that TSConfig does not include this file. Either: -- Change ESLint's list of included files to not include this file -- Change that TSConfig to include this file -- Create a new TSConfig that includes this file and include it in your parserOptions.project -See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" -`; - -exports[`parseAndGenerateServices invalid file error messages project includes errors for not included files 2`] = ` -"ESLint was configured to run on \`/ts/notIncluded02.tsx\` using \`parserOptions.project\`: /tsconfig.json -However, that TSConfig does not include this file. Either: -- Change ESLint's list of included files to not include this file -- Change that TSConfig to include this file -- Create a new TSConfig that includes this file and include it in your parserOptions.project -See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" -`; - -exports[`parseAndGenerateServices invalid file error messages project includes errors for not included files 3`] = ` -"ESLint was configured to run on \`/js/notIncluded01.js\` using \`parserOptions.project\`: /tsconfig.json -However, that TSConfig does not include this file. Either: -- Change ESLint's list of included files to not include this file -- Change that TSConfig to include this file -- Create a new TSConfig that includes this file and include it in your parserOptions.project -See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" -`; - -exports[`parseAndGenerateServices invalid file error messages project includes errors for not included files 4`] = ` -"ESLint was configured to run on \`/js/notIncluded02.jsx\` using \`parserOptions.project\`: /tsconfig.json -However, that TSConfig does not include this file. Either: -- Change ESLint's list of included files to not include this file -- Change that TSConfig to include this file -- Create a new TSConfig that includes this file and include it in your parserOptions.project -See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" -`; - -exports[`parseAndGenerateServices invalid project error messages throws when non of multiple projects include the file 1`] = ` -"ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: -- /tsconfig.json -- /tsconfig.extra.json -However, none of those TSConfigs include this file. Either: -- Change ESLint's list of included files to not include this file -- Change one of those TSConfigs to include this file -- Create a new TSConfig that includes this file and include it in your parserOptions.project -See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" -`; - exports[`parseAndGenerateServices isolated parsing should parse .js file - with JSX content - parserOptions.jsx = false 1`] = ` { "ast": { diff --git a/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap index 0d94ba3a46b8..79772deb70e0 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/semanticInfo.test.ts.snap @@ -1734,5 +1734,3 @@ exports[`semanticInfo fixtures/non-existent-estree-nodes.src 1`] = ` "type": "Program", } `; - -exports[`semanticInfo malformed project file 1`] = `"Compiler option 'compileOnSave' requires a value of type boolean."`; diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts new file mode 100644 index 000000000000..f6f6d117e3bc --- /dev/null +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -0,0 +1,7 @@ +import { createProjectService } from '../../src/create-program/createProjectService'; + +describe('createProjectService', () => { + it('does not crash', () => { + createProjectService(); + }); +}); diff --git a/packages/typescript-estree/tests/lib/parse.project-true.test.ts b/packages/typescript-estree/tests/lib/parse.project-true.test.ts index ff2ea0d0e3fb..ca81ab99f506 100644 --- a/packages/typescript-estree/tests/lib/parse.project-true.test.ts +++ b/packages/typescript-estree/tests/lib/parse.project-true.test.ts @@ -35,15 +35,17 @@ describe('parseAndGenerateServices', () => { }); }); - it('throws an error when a parent project does not exist', () => { - expect(() => - parser.parseAndGenerateServices('const a = true', { - ...config, - filePath: join(PROJECT_DIR, 'notIncluded.ts'), - }), - ).toThrow( - /project was set to `true` but couldn't find any tsconfig.json relative to '.+[/\\]tests[/\\]fixtures[/\\]projectTrue[/\\]notIncluded.ts' within '.+[/\\]tests[/\\]fixtures[/\\]projectTrue'./, - ); - }); + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + it('throws an error when a parent project does not exist', () => { + expect(() => + parser.parseAndGenerateServices('const a = true', { + ...config, + filePath: join(PROJECT_DIR, 'notIncluded.ts'), + }), + ).toThrow( + /project was set to `true` but couldn't find any tsconfig.json relative to '.+[/\\]tests[/\\]fixtures[/\\]projectTrue[/\\]notIncluded.ts' within '.+[/\\]tests[/\\]fixtures[/\\]projectTrue'./, + ); + }); + } }); }); diff --git a/packages/typescript-estree/tests/lib/parse.test.ts b/packages/typescript-estree/tests/lib/parse.test.ts index 02bacdfadf3a..658493c9150c 100644 --- a/packages/typescript-estree/tests/lib/parse.test.ts +++ b/packages/typescript-estree/tests/lib/parse.test.ts @@ -166,6 +166,7 @@ describe('parseAndGenerateServices', () => { describe('isolated parsing', () => { const config: TSESTreeOptions = { + EXPERIMENTAL_useProjectService: false, comment: true, tokens: true, range: true, @@ -339,124 +340,184 @@ describe('parseAndGenerateServices', () => { }); }); - describe('invalid file error messages', () => { - const PROJECT_DIR = resolve(FIXTURES_DIR, '../invalidFileErrors'); - const code = 'var a = true'; - const config: TSESTreeOptions = { - comment: true, - tokens: true, - range: true, - loc: true, - tsconfigRootDir: PROJECT_DIR, - project: './tsconfig.json', - }; - const testParse = - (filePath: string, extraFileExtensions: string[] = ['.vue']) => - (): void => { - try { - parser.parseAndGenerateServices(code, { - ...config, - extraFileExtensions, - filePath: join(PROJECT_DIR, filePath), - }); - } catch (error) { - throw alignErrorPath(error as Error); - } + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + describe('invalid file error messages', () => { + const PROJECT_DIR = resolve(FIXTURES_DIR, '../invalidFileErrors'); + const code = 'var a = true'; + const config: TSESTreeOptions = { + comment: true, + tokens: true, + range: true, + loc: true, + tsconfigRootDir: PROJECT_DIR, + project: './tsconfig.json', }; + const testParse = + (filePath: string, extraFileExtensions: string[] = ['.vue']) => + (): void => { + try { + parser.parseAndGenerateServices(code, { + ...config, + extraFileExtensions, + filePath: join(PROJECT_DIR, filePath), + }); + } catch (error) { + throw alignErrorPath(error as Error); + } + }; + + describe('project includes', () => { + it("doesn't error for matched files", () => { + expect(testParse('ts/included01.ts')).not.toThrow(); + expect(testParse('ts/included02.tsx')).not.toThrow(); + expect(testParse('js/included01.js')).not.toThrow(); + expect(testParse('js/included02.jsx')).not.toThrow(); + }); - describe('project includes', () => { - it("doesn't error for matched files", () => { - expect(testParse('ts/included01.ts')).not.toThrow(); - expect(testParse('ts/included02.tsx')).not.toThrow(); - expect(testParse('js/included01.js')).not.toThrow(); - expect(testParse('js/included02.jsx')).not.toThrow(); + it('errors for not included files', () => { + expect(testParse('ts/notIncluded0j1.ts')) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: /tsconfig.json + However, that TSConfig does not include this file. Either: + - Change ESLint's list of included files to not include this file + - Change that TSConfig to include this file + - Create a new TSConfig that includes this file and include it in your parserOptions.project + See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" + `); + expect(testParse('ts/notIncluded02.tsx')) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/ts/notIncluded02.tsx\` using \`parserOptions.project\`: /tsconfig.json + However, that TSConfig does not include this file. Either: + - Change ESLint's list of included files to not include this file + - Change that TSConfig to include this file + - Create a new TSConfig that includes this file and include it in your parserOptions.project + See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" + `); + expect(testParse('js/notIncluded01.js')) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/js/notIncluded01.js\` using \`parserOptions.project\`: /tsconfig.json + However, that TSConfig does not include this file. Either: + - Change ESLint's list of included files to not include this file + - Change that TSConfig to include this file + - Create a new TSConfig that includes this file and include it in your parserOptions.project + See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" + `); + expect(testParse('js/notIncluded02.jsx')) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/js/notIncluded02.jsx\` using \`parserOptions.project\`: /tsconfig.json + However, that TSConfig does not include this file. Either: + - Change ESLint's list of included files to not include this file + - Change that TSConfig to include this file + - Create a new TSConfig that includes this file and include it in your parserOptions.project + See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" + `); + }); }); - it('errors for not included files', () => { - expect( - testParse('ts/notIncluded0j1.ts'), - ).toThrowErrorMatchingSnapshot(); - expect( - testParse('ts/notIncluded02.tsx'), - ).toThrowErrorMatchingSnapshot(); - expect(testParse('js/notIncluded01.js')).toThrowErrorMatchingSnapshot(); - expect( - testParse('js/notIncluded02.jsx'), - ).toThrowErrorMatchingSnapshot(); - }); - }); + describe('"parserOptions.extraFileExtensions" is empty', () => { + it('should not error', () => { + expect(testParse('ts/included01.ts', [])).not.toThrow(); + }); - describe('"parserOptions.extraFileExtensions" is empty', () => { - it('should not error', () => { - expect(testParse('ts/included01.ts', [])).not.toThrow(); + it('the extension does not match', () => { + expect(testParse('other/unknownFileType.unknown', [])) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json + The extension for the file (\`.unknown\`) is non-standard. You should add \`parserOptions.extraFileExtensions\` to your config." + `); + }); }); - it('the extension does not match', () => { - expect( - testParse('other/unknownFileType.unknown', []), - ).toThrowErrorMatchingSnapshot(); - }); - }); + describe('"parserOptions.extraFileExtensions" is non-empty', () => { + describe('the extension matches', () => { + it('the file is included', () => { + expect(testParse('other/included.vue')).not.toThrow(); + }); - describe('"parserOptions.extraFileExtensions" is non-empty', () => { - describe('the extension matches', () => { - it('the file is included', () => { - expect(testParse('other/included.vue')).not.toThrow(); - }); + it("the file isn't included", () => { + expect(testParse('other/notIncluded.vue')) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/other/notIncluded.vue\` using \`parserOptions.project\`: /tsconfig.json + However, that TSConfig does not include this file. Either: + - Change ESLint's list of included files to not include this file + - Change that TSConfig to include this file + - Create a new TSConfig that includes this file and include it in your parserOptions.project + See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" + `); + }); - it("the file isn't included", () => { - expect( - testParse('other/notIncluded.vue'), - ).toThrowErrorMatchingSnapshot(); + it('duplicate extension', () => { + expect(testParse('ts/notIncluded.ts', ['.ts'])) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/ts/notIncluded.ts\` using \`parserOptions.project\`: /tsconfig.json + You unnecessarily included the extension \`.ts\` with the \`parserOptions.extraFileExtensions\` option. This extension is already handled by the parser by default. + However, that TSConfig does not include this file. Either: + - Change ESLint's list of included files to not include this file + - Change that TSConfig to include this file + - Create a new TSConfig that includes this file and include it in your parserOptions.project + See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" + `); + }); }); - it('duplicate extension', () => { - expect( - testParse('ts/notIncluded.ts', ['.ts']), - ).toThrowErrorMatchingSnapshot(); + it('invalid extension', () => { + expect(testParse('other/unknownFileType.unknown', ['unknown'])) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json + Found unexpected extension \`unknown\` specified with the \`parserOptions.extraFileExtensions\` option. Did you mean \`.unknown\`? + The extension for the file (\`.unknown\`) is non-standard. It should be added to your existing \`parserOptions.extraFileExtensions\`." + `); }); - }); - - it('invalid extension', () => { - expect( - testParse('other/unknownFileType.unknown', ['unknown']), - ).toThrowErrorMatchingSnapshot(); - }); - it('the extension does not match', () => { - expect( - testParse('other/unknownFileType.unknown'), - ).toThrowErrorMatchingSnapshot(); + it('the extension does not match', () => { + expect(testParse('other/unknownFileType.unknown')) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/other/unknownFileType.unknown\` using \`parserOptions.project\`: /tsconfig.json + The extension for the file (\`.unknown\`) is non-standard. It should be added to your existing \`parserOptions.extraFileExtensions\`." + `); + }); }); }); - }); - describe('invalid project error messages', () => { - it('throws when non of multiple projects include the file', () => { - const PROJECT_DIR = resolve(FIXTURES_DIR, '../invalidFileErrors'); - const code = 'var a = true'; - const config: TSESTreeOptions = { - comment: true, - tokens: true, - range: true, - loc: true, - tsconfigRootDir: PROJECT_DIR, - project: ['./**/tsconfig.json', './**/tsconfig.extra.json'], - }; - const testParse = (filePath: string) => (): void => { - try { - parser.parseAndGenerateServices(code, { - ...config, - filePath: join(PROJECT_DIR, filePath), - }); - } catch (error) { - throw alignErrorPath(error as Error); - } - }; - - expect(testParse('ts/notIncluded0j1.ts')).toThrowErrorMatchingSnapshot(); + describe('invalid project error messages', () => { + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + it('throws when none of multiple projects include the file', () => { + const PROJECT_DIR = resolve(FIXTURES_DIR, '../invalidFileErrors'); + const code = 'var a = true'; + const config: TSESTreeOptions = { + comment: true, + tokens: true, + range: true, + loc: true, + tsconfigRootDir: PROJECT_DIR, + project: ['./**/tsconfig.json', './**/tsconfig.extra.json'], + }; + const testParse = (filePath: string) => (): void => { + try { + parser.parseAndGenerateServices(code, { + ...config, + filePath: join(PROJECT_DIR, filePath), + }); + } catch (error) { + throw alignErrorPath(error as Error); + } + }; + + expect(testParse('ts/notIncluded0j1.ts')) + .toThrowErrorMatchingInlineSnapshot(` + "ESLint was configured to run on \`/ts/notIncluded0j1.ts\` using \`parserOptions.project\`: + - /tsconfig.json + - /tsconfig.extra.json + However, none of those TSConfigs include this file. Either: + - Change ESLint's list of included files to not include this file + - Change one of those TSConfigs to include this file + - Create a new TSConfig that includes this file and include it in your parserOptions.project + See the typescript-eslint docs for more info: https://typescript-eslint.io/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file" + `); + }); + } }); - }); + } describe('debug options', () => { const debugEnable = jest.fn(); @@ -499,123 +560,127 @@ describe('parseAndGenerateServices', () => { ); }); - it('should turn on typescript debugger', () => { - expect(() => - parser.parseAndGenerateServices('const x = 1;', { - debugLevel: ['typescript'], - filePath: './path-that-doesnt-exist.ts', - project: ['./tsconfig-that-doesnt-exist.json'], - }), - ) // should throw because the file and tsconfig don't exist - .toThrow(); - expect(createDefaultCompilerOptionsFromExtra).toHaveBeenCalled(); - expect(createDefaultCompilerOptionsFromExtra).toHaveReturnedWith( - expect.objectContaining({ - extendedDiagnostics: true, - }), - ); - }); + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + it('should turn on typescript debugger', () => { + expect(() => + parser.parseAndGenerateServices('const x = 1;', { + debugLevel: ['typescript'], + filePath: './path-that-doesnt-exist.ts', + project: ['./tsconfig-that-doesnt-exist.json'], + }), + ) // should throw because the file and tsconfig don't exist + .toThrow(); + expect(createDefaultCompilerOptionsFromExtra).toHaveBeenCalled(); + expect(createDefaultCompilerOptionsFromExtra).toHaveReturnedWith( + expect.objectContaining({ + extendedDiagnostics: true, + }), + ); + }); + } }); - describe('projectFolderIgnoreList', () => { - beforeEach(() => { - parser.clearCaches(); - }); - - const PROJECT_DIR = resolve(FIXTURES_DIR, '../projectFolderIgnoreList'); - const code = 'var a = true'; - const config: TSESTreeOptions = { - comment: true, - tokens: true, - range: true, - loc: true, - tsconfigRootDir: PROJECT_DIR, - project: './**/tsconfig.json', - }; + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + describe('projectFolderIgnoreList', () => { + beforeEach(() => { + parser.clearCaches(); + }); - const testParse = - ( - filePath: 'ignoreme' | 'includeme', - projectFolderIgnoreList?: TSESTreeOptions['projectFolderIgnoreList'], - ) => - (): void => { - parser.parseAndGenerateServices(code, { - ...config, - projectFolderIgnoreList, - filePath: join(PROJECT_DIR, filePath, './file.ts'), - }); + const PROJECT_DIR = resolve(FIXTURES_DIR, '../projectFolderIgnoreList'); + const code = 'var a = true'; + const config: TSESTreeOptions = { + comment: true, + tokens: true, + range: true, + loc: true, + tsconfigRootDir: PROJECT_DIR, + project: './**/tsconfig.json', }; - it('ignores nothing when given nothing', () => { - expect(testParse('ignoreme')).not.toThrow(); - expect(testParse('includeme')).not.toThrow(); - }); + const testParse = + ( + filePath: 'ignoreme' | 'includeme', + projectFolderIgnoreList?: TSESTreeOptions['projectFolderIgnoreList'], + ) => + (): void => { + parser.parseAndGenerateServices(code, { + ...config, + projectFolderIgnoreList, + filePath: join(PROJECT_DIR, filePath, './file.ts'), + }); + }; + + it('ignores nothing when given nothing', () => { + expect(testParse('ignoreme')).not.toThrow(); + expect(testParse('includeme')).not.toThrow(); + }); - it('ignores a folder when given a string glob', () => { - const ignore = ['**/ignoreme/**']; - // cspell:disable-next-line - expect(testParse('ignoreme', ignore)).toThrow(); - // cspell:disable-next-line - expect(testParse('includeme', ignore)).not.toThrow(); + it('ignores a folder when given a string glob', () => { + const ignore = ['**/ignoreme/**']; + // cspell:disable-next-line + expect(testParse('ignoreme', ignore)).toThrow(); + // cspell:disable-next-line + expect(testParse('includeme', ignore)).not.toThrow(); + }); }); - }); - describe('cacheLifetime', () => { - describe('glob', () => { - function doParse(lifetime: CacheDurationSeconds): void { - parser.parseAndGenerateServices('const x = 1', { - cacheLifetime: { - glob: lifetime, - }, - filePath: join(FIXTURES_DIR, 'file.ts'), - tsconfigRootDir: FIXTURES_DIR, - project: ['./**/tsconfig.json', './**/tsconfig.extra.json'], - }); - } + describe('cacheLifetime', () => { + describe('glob', () => { + function doParse(lifetime: CacheDurationSeconds): void { + parser.parseAndGenerateServices('const x = 1', { + cacheLifetime: { + glob: lifetime, + }, + filePath: join(FIXTURES_DIR, 'file.ts'), + tsconfigRootDir: FIXTURES_DIR, + project: ['./**/tsconfig.json', './**/tsconfig.extra.json'], + }); + } - it('should cache globs if the lifetime is non-zero', () => { - doParse(30); - expect(globbySyncMock).toHaveBeenCalledTimes(1); - doParse(30); - // shouldn't call globby again due to the caching - expect(globbySyncMock).toHaveBeenCalledTimes(1); - }); + it('should cache globs if the lifetime is non-zero', () => { + doParse(30); + expect(globbySyncMock).toHaveBeenCalledTimes(1); + doParse(30); + // shouldn't call globby again due to the caching + expect(globbySyncMock).toHaveBeenCalledTimes(1); + }); - it('should not cache globs if the lifetime is zero', () => { - doParse(0); - expect(globbySyncMock).toHaveBeenCalledTimes(1); - doParse(0); - // should call globby again because we specified immediate cache expiry - expect(globbySyncMock).toHaveBeenCalledTimes(2); - }); + it('should not cache globs if the lifetime is zero', () => { + doParse(0); + expect(globbySyncMock).toHaveBeenCalledTimes(1); + doParse(0); + // should call globby again because we specified immediate cache expiry + expect(globbySyncMock).toHaveBeenCalledTimes(2); + }); - it('should evict the cache if the entry expires', () => { - hrtimeSpy.mockReturnValueOnce([1, 0]); + it('should evict the cache if the entry expires', () => { + hrtimeSpy.mockReturnValueOnce([1, 0]); - doParse(30); - expect(globbySyncMock).toHaveBeenCalledTimes(1); + doParse(30); + expect(globbySyncMock).toHaveBeenCalledTimes(1); - // wow so much time has passed - hrtimeSpy.mockReturnValueOnce([Number.MAX_VALUE, 0]); + // wow so much time has passed + hrtimeSpy.mockReturnValueOnce([Number.MAX_VALUE, 0]); - doParse(30); - // shouldn't call globby again due to the caching - expect(globbySyncMock).toHaveBeenCalledTimes(2); - }); + doParse(30); + // shouldn't call globby again due to the caching + expect(globbySyncMock).toHaveBeenCalledTimes(2); + }); - it('should infinitely cache if passed Infinity', () => { - hrtimeSpy.mockReturnValueOnce([1, 0]); + it('should infinitely cache if passed Infinity', () => { + hrtimeSpy.mockReturnValueOnce([1, 0]); - doParse('Infinity'); - expect(globbySyncMock).toHaveBeenCalledTimes(1); + doParse('Infinity'); + expect(globbySyncMock).toHaveBeenCalledTimes(1); - // wow so much time has passed - hrtimeSpy.mockReturnValueOnce([Number.MAX_VALUE, 0]); + // wow so much time has passed + hrtimeSpy.mockReturnValueOnce([Number.MAX_VALUE, 0]); - doParse('Infinity'); - // shouldn't call globby again due to the caching - expect(globbySyncMock).toHaveBeenCalledTimes(1); + doParse('Infinity'); + // shouldn't call globby again due to the caching + expect(globbySyncMock).toHaveBeenCalledTimes(1); + }); }); }); - }); + } }); diff --git a/packages/typescript-estree/tests/lib/persistentParse.test.ts b/packages/typescript-estree/tests/lib/persistentParse.test.ts index 63e81d7e260a..710b9c54ab5a 100644 --- a/packages/typescript-estree/tests/lib/persistentParse.test.ts +++ b/packages/typescript-estree/tests/lib/persistentParse.test.ts @@ -2,6 +2,7 @@ import fs from 'fs'; import path from 'path'; import tmp from 'tmp'; +import { clearCaches } from '../../src/clear-caches'; import { clearWatchCaches } from '../../src/create-program/getWatchProgramsForProjects'; import { parseAndGenerateServices } from '../../src/parser'; @@ -86,6 +87,11 @@ function baseTests( tsConfigExcludeBar: Record, tsConfigIncludeAll: Record, ): void { + // The experimental project server creates a default project for files + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true') { + return; + } + it('parses both files successfully when included', () => { const PROJECT_DIR = setup(tsConfigIncludeAll); @@ -177,6 +183,7 @@ function baseTests( // change the config file so it now includes all files writeTSConfig(PROJECT_DIR, tsConfigIncludeAll); + clearCaches(); expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); @@ -257,43 +264,48 @@ describe('persistent parse', () => { /* If there is no includes, then typescript will ask for a slightly different set of watchers. */ - describe('tsconfig with no includes / files', () => { - const tsConfigExcludeBar = { - exclude: ['./src/bar.ts'], - }; - const tsConfigIncludeAll = {}; - baseTests(tsConfigExcludeBar, tsConfigIncludeAll); + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + describe('tsconfig with no includes / files', () => { + const tsConfigExcludeBar = { + exclude: ['./src/bar.ts'], + }; + const tsConfigIncludeAll = {}; - it('handles tsconfigs with no includes/excludes (single level)', () => { - const PROJECT_DIR = setup({}, false); + baseTests(tsConfigExcludeBar, tsConfigIncludeAll); - // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); + it('handles tsconfigs with no includes/excludes (single level)', () => { + const PROJECT_DIR = setup({}, false); - // write a new file and attempt to parse it - writeFile(PROJECT_DIR, 'bar'); + // parse once to: assert the config as correct, and to make sure the program is setup + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).toThrow(); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); - }); + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, 'bar'); + clearCaches(); + + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('bar', PROJECT_DIR)).not.toThrow(); + }); - it('handles tsconfigs with no includes/excludes (nested)', () => { - const PROJECT_DIR = setup({}, false); - const bazSlashBar = 'baz/bar' as const; + it('handles tsconfigs with no includes/excludes (nested)', () => { + const PROJECT_DIR = setup({}, false); + const bazSlashBar = 'baz/bar' as const; - // parse once to: assert the config as correct, and to make sure the program is setup - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); + // parse once to: assert the config as correct, and to make sure the program is setup + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).toThrow(); - // write a new file and attempt to parse it - writeFile(PROJECT_DIR, bazSlashBar); + // write a new file and attempt to parse it + writeFile(PROJECT_DIR, bazSlashBar); + clearCaches(); - expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); - expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); + expect(() => parseFile('foo', PROJECT_DIR)).not.toThrow(); + expect(() => parseFile(bazSlashBar, PROJECT_DIR)).not.toThrow(); + }); }); - }); + } /* If there is no includes, then typescript will ask for a slightly different set of watchers. diff --git a/packages/typescript-estree/tests/lib/semanticInfo-singleRun.test.ts b/packages/typescript-estree/tests/lib/semanticInfo-singleRun.test.ts index 725195111f14..09daae795d61 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo-singleRun.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo-singleRun.test.ts @@ -135,124 +135,132 @@ describe('semanticInfo - singleRun', () => { process.env.CI = originalEnvCI; }); - it('should lazily create the required program out of the provided "parserOptions.project" one time when TSESTREE_SINGLE_RUN=true', () => { - /** - * Single run because of explicit environment variable TSESTREE_SINGLE_RUN - */ - const originalTSESTreeSingleRun = process.env.TSESTREE_SINGLE_RUN; - process.env.TSESTREE_SINGLE_RUN = 'true'; - - const resultProgram = parseAndGenerateServices(code, options).services - .program; - expect(resultProgram).toEqual(mockProgram); - - // Call parseAndGenerateServices() again to ensure caching of Programs is working correctly... - parseAndGenerateServices(code, options); - // ...by asserting this was only called once per project - expect(createProgramFromConfigFile).toHaveBeenCalledTimes(tsconfigs.length); - - expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( - 1, - resolvedProject(tsconfigs[0]), - ); - expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( - 2, - resolvedProject(tsconfigs[1]), - ); - - // Restore process data - process.env.TSESTREE_SINGLE_RUN = originalTSESTreeSingleRun; - }); - - it('should lazily create the required program out of the provided "parserOptions.project" one time when singleRun is inferred from CI=true', () => { - /** - * Single run because of CI=true (we need to make sure we respect the original value - * so that we won't interfere with our own usage of the variable) - */ - const originalEnvCI = process.env.CI; - process.env.CI = 'true'; - - const resultProgram = parseAndGenerateServices(code, options).services - .program; - expect(resultProgram).toEqual(mockProgram); - - // Call parseAndGenerateServices() again to ensure caching of Programs is working correctly... - parseAndGenerateServices(code, options); - // ...by asserting this was only called once per project - expect(createProgramFromConfigFile).toHaveBeenCalledTimes(tsconfigs.length); - - expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( - 1, - resolvedProject(tsconfigs[0]), - ); - expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( - 2, - resolvedProject(tsconfigs[1]), - ); - - // Restore process data - process.env.CI = originalEnvCI; - }); - - it('should lazily create the required program out of the provided "parserOptions.project" one time when singleRun is inferred from process.argv', () => { - /** - * Single run because of process.argv - */ - const originalProcessArgv = process.argv; - process.argv = ['', path.normalize('node_modules/.bin/eslint'), '']; - - const resultProgram = parseAndGenerateServices(code, options).services - .program; - expect(resultProgram).toEqual(mockProgram); - - // Call parseAndGenerateServices() again to ensure caching of Programs is working correctly... - parseAndGenerateServices(code, options); - // ...by asserting this was only called once per project - expect(createProgramFromConfigFile).toHaveBeenCalledTimes(tsconfigs.length); - - expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( - 1, - resolvedProject(tsconfigs[0]), - ); - expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( - 2, - resolvedProject(tsconfigs[1]), - ); - - // Restore process data - process.argv = originalProcessArgv; - }); - - it('should stop iterating through and lazily creating programs for the given "parserOptions.project" once a matching one has been found', () => { - /** - * Single run because of explicit environment variable TSESTREE_SINGLE_RUN - */ - const originalTSESTreeSingleRun = process.env.TSESTREE_SINGLE_RUN; - process.env.TSESTREE_SINGLE_RUN = 'true'; - - const optionsWithReversedTsconfigs = { - ...options, - // Now the matching tsconfig comes first - project: [...options.project].reverse(), - }; - - const resultProgram = parseAndGenerateServices( - code, - optionsWithReversedTsconfigs, - ).services.program; - expect(resultProgram).toEqual(mockProgram); - - // Call parseAndGenerateServices() again to ensure caching of Programs is working correctly... - parseAndGenerateServices(code, options); - // ...by asserting this was only called only once - expect(createProgramFromConfigFile).toHaveBeenCalledTimes(1); - - expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( - 1, - resolvedProject(tsconfigs[1]), - ); - - // Restore process data - process.env.TSESTREE_SINGLE_RUN = originalTSESTreeSingleRun; - }); + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + it('should lazily create the required program out of the provided "parserOptions.project" one time when TSESTREE_SINGLE_RUN=true', () => { + /** + * Single run because of explicit environment variable TSESTREE_SINGLE_RUN + */ + const originalTSESTreeSingleRun = process.env.TSESTREE_SINGLE_RUN; + process.env.TSESTREE_SINGLE_RUN = 'true'; + + const resultProgram = parseAndGenerateServices(code, options).services + .program; + expect(resultProgram).toEqual(mockProgram); + + // Call parseAndGenerateServices() again to ensure caching of Programs is working correctly... + parseAndGenerateServices(code, options); + // ...by asserting this was only called once per project + expect(createProgramFromConfigFile).toHaveBeenCalledTimes( + tsconfigs.length, + ); + + expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( + 1, + resolvedProject(tsconfigs[0]), + ); + expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( + 2, + resolvedProject(tsconfigs[1]), + ); + + // Restore process data + process.env.TSESTREE_SINGLE_RUN = originalTSESTreeSingleRun; + }); + + it('should lazily create the required program out of the provided "parserOptions.project" one time when singleRun is inferred from CI=true', () => { + /** + * Single run because of CI=true (we need to make sure we respect the original value + * so that we won't interfere with our own usage of the variable) + */ + const originalEnvCI = process.env.CI; + process.env.CI = 'true'; + + const resultProgram = parseAndGenerateServices(code, options).services + .program; + expect(resultProgram).toEqual(mockProgram); + + // Call parseAndGenerateServices() again to ensure caching of Programs is working correctly... + parseAndGenerateServices(code, options); + // ...by asserting this was only called once per project + expect(createProgramFromConfigFile).toHaveBeenCalledTimes( + tsconfigs.length, + ); + + expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( + 1, + resolvedProject(tsconfigs[0]), + ); + expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( + 2, + resolvedProject(tsconfigs[1]), + ); + + // Restore process data + process.env.CI = originalEnvCI; + }); + + it('should lazily create the required program out of the provided "parserOptions.project" one time when singleRun is inferred from process.argv', () => { + /** + * Single run because of process.argv + */ + const originalProcessArgv = process.argv; + process.argv = ['', path.normalize('node_modules/.bin/eslint'), '']; + + const resultProgram = parseAndGenerateServices(code, options).services + .program; + expect(resultProgram).toEqual(mockProgram); + + // Call parseAndGenerateServices() again to ensure caching of Programs is working correctly... + parseAndGenerateServices(code, options); + // ...by asserting this was only called once per project + expect(createProgramFromConfigFile).toHaveBeenCalledTimes( + tsconfigs.length, + ); + + expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( + 1, + resolvedProject(tsconfigs[0]), + ); + expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( + 2, + resolvedProject(tsconfigs[1]), + ); + + // Restore process data + process.argv = originalProcessArgv; + }); + + it('should stop iterating through and lazily creating programs for the given "parserOptions.project" once a matching one has been found', () => { + /** + * Single run because of explicit environment variable TSESTREE_SINGLE_RUN + */ + const originalTSESTreeSingleRun = process.env.TSESTREE_SINGLE_RUN; + process.env.TSESTREE_SINGLE_RUN = 'true'; + + const optionsWithReversedTsconfigs = { + ...options, + // Now the matching tsconfig comes first + project: [...options.project].reverse(), + }; + + const resultProgram = parseAndGenerateServices( + code, + optionsWithReversedTsconfigs, + ).services.program; + expect(resultProgram).toEqual(mockProgram); + + // Call parseAndGenerateServices() again to ensure caching of Programs is working correctly... + parseAndGenerateServices(code, options); + // ...by asserting this was only called only once + expect(createProgramFromConfigFile).toHaveBeenCalledTimes(1); + + expect(createProgramFromConfigFile).toHaveBeenNthCalledWith( + 1, + resolvedProject(tsconfigs[1]), + ); + + // Restore process data + process.env.TSESTREE_SINGLE_RUN = originalTSESTreeSingleRun; + }); + } }); diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 3ebb735eb7f6..83569479085c 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -3,7 +3,7 @@ import glob = require('glob'); import * as path from 'path'; import * as ts from 'typescript'; -import { clearWatchCaches } from '../../src/create-program/getWatchProgramsForProjects'; +import { clearCaches } from '../../src'; import { createProgramFromConfigFile as createProgram } from '../../src/create-program/useProvidedPrograms'; import type { ParseAndGenerateServicesResult } from '../../src/parser'; import { parseAndGenerateServices } from '../../src/parser'; @@ -37,7 +37,7 @@ function createOptions(fileName: string): TSESTreeOptions & { cwd?: string } { } // ensure tsconfig-parser watch caches are clean for each test -beforeEach(() => clearWatchCaches()); +beforeEach(() => clearCaches()); describe('semanticInfo', () => { // test all AST snapshots @@ -246,55 +246,61 @@ describe('semanticInfo', () => { expect(parseResult.services.program).toBeDefined(); }); - it(`non-existent file should throw error when project provided`, () => { - expect(() => - parseCodeAndGenerateServices( - `function M() { return Base }`, - createOptions(''), - ), - ).toThrow( - /ESLint was configured to run on `\/estree\.ts` using/, - ); - }); - - it('non-existent project file', () => { - const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); - const badConfig = createOptions(fileName); - badConfig.project = './tsconfigs.json'; - expect(() => - parseCodeAndGenerateServices( - fs.readFileSync(fileName, 'utf8'), - badConfig, - ), - ).toThrow(/Cannot read file .+tsconfigs\.json'/); - }); + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + it(`non-existent file should throw error when project provided`, () => { + expect(() => + parseCodeAndGenerateServices( + `function M() { return Base }`, + createOptions(''), + ), + ).toThrow( + /ESLint was configured to run on `\/estree\.ts` using/, + ); + }); + } + + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + it('non-existent project file', () => { + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const badConfig = createOptions(fileName); + badConfig.project = './tsconfigs.json'; + expect(() => + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), + ).toThrow(/Cannot read file .+tsconfigs\.json'/); + }); - it('fail to read project file', () => { - const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); - const badConfig = createOptions(fileName); - badConfig.project = '.'; - expect(() => - parseCodeAndGenerateServices( - fs.readFileSync(fileName, 'utf8'), - badConfig, - ), - ).toThrow( - // case insensitive because unix based systems are case insensitive - /Cannot read file .+semanticInfo'./i, - ); - }); + it('fail to read project file', () => { + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const badConfig = createOptions(fileName); + badConfig.project = '.'; + expect(() => + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), + ).toThrow( + // case insensitive because unix based systems are case insensitive + /Cannot read file .+semanticInfo'/i, + ); + }); - it('malformed project file', () => { - const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); - const badConfig = createOptions(fileName); - badConfig.project = './badTSConfig/tsconfig.json'; - expect(() => - parseCodeAndGenerateServices( - fs.readFileSync(fileName, 'utf8'), - badConfig, - ), - ).toThrowErrorMatchingSnapshot(); - }); + it('malformed project file', () => { + const fileName = path.resolve(FIXTURES_DIR, 'isolated-file.src.ts'); + const badConfig = createOptions(fileName); + badConfig.project = './badTSConfig/tsconfig.json'; + expect(() => + parseCodeAndGenerateServices( + fs.readFileSync(fileName, 'utf8'), + badConfig, + ), + ).toThrowErrorMatchingInlineSnapshot( + `"Compiler option 'compileOnSave' requires a value of type boolean."`, + ); + }); + } it('default program produced with option', () => { const parseResult = parseCodeAndGenerateServices('var foo = 5;', { @@ -314,20 +320,22 @@ describe('semanticInfo', () => { ); }); - it(`first matching provided program instance is returned in result`, () => { - const filename = testFiles[0]; - const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); - const program2 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); - const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); - const options = createOptions(filename); - const optionsProjectString = { - ...options, - programs: [program1, program2], - project: './tsconfig.json', - }; - const parseResult = parseAndGenerateServices(code, optionsProjectString); - expect(parseResult.services.program).toBe(program1); - }); + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + it(`first matching provided program instance is returned in result`, () => { + const filename = testFiles[0]; + const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const program2 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const code = fs.readFileSync(path.join(FIXTURES_DIR, filename), 'utf8'); + const options = createOptions(filename); + const optionsProjectString = { + ...options, + programs: [program1, program2], + project: './tsconfig.json', + }; + const parseResult = parseAndGenerateServices(code, optionsProjectString); + expect(parseResult.services.program).toBe(program1); + }); + } it('file not in provided program instance(s)', () => { const filename = 'non-existent-file.ts'; @@ -369,6 +377,10 @@ describe('semanticInfo', () => { function testIsolatedFile( parseResult: ParseAndGenerateServicesResult, ): void { + if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true') { + return; + } + // get type checker expectToHaveParserServices(parseResult.services); const checker = parseResult.services.program.getTypeChecker(); diff --git a/packages/utils/src/ts-eslint/Rule.ts b/packages/utils/src/ts-eslint/Rule.ts index 4ff0546bb65c..f15e92bdb38c 100644 --- a/packages/utils/src/ts-eslint/Rule.ts +++ b/packages/utils/src/ts-eslint/Rule.ts @@ -223,7 +223,7 @@ interface RuleContext< * It is a path to a directory that should be considered as the current working directory. * @since 6.6.0 */ - getCwd?(): string; + getCwd(): string; /** * Returns the filename associated with the source. diff --git a/packages/website/src/components/linter/config.ts b/packages/website/src/components/linter/config.ts index f728dc1a6355..fc6389b4e1b2 100644 --- a/packages/website/src/components/linter/config.ts +++ b/packages/website/src/components/linter/config.ts @@ -13,6 +13,7 @@ export const defaultParseSettings: ParseSettings = { DEPRECATED__createDefaultProgram: false, errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: false, + EXPERIMENTAL_projectService: undefined, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: false, extraFileExtensions: [], filePath: '', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy