diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index 610f641d384d..27122374bd18 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -167,7 +167,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { * * @see https://github.com/typescript-eslint/typescript-eslint/issues/6575 */ - EXPERIMENTAL_useProjectService?: boolean; + EXPERIMENTAL_useProjectService?: boolean | ProjectServiceOptions; /** * ***EXPERIMENTAL FLAG*** - Use this at your own risk. @@ -270,6 +270,16 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { }; } +/** + * Granular options to configure the project service. + */ +interface ProjectServiceOptions { + /** + * Globs of files to allow running with the default inferred project settings. + */ + allowDefaultProjectForFiles?: string[]; +} + interface ParserServices { program: ts.Program; esTreeNodeToTSNodeMap: WeakMap; diff --git a/packages/eslint-plugin-tslint/tests/index.spec.ts b/packages/eslint-plugin-tslint/tests/index.spec.ts index 59f84be30770..bbe479c832b8 100644 --- a/packages/eslint-plugin-tslint/tests/index.spec.ts +++ b/packages/eslint-plugin-tslint/tests/index.spec.ts @@ -166,71 +166,73 @@ ruleTester.run('tslint/config', rule, { ], }); -describe('tslint/error', () => { - function testOutput(code: string, config: ClassicConfig.Config): void { - const linter = new TSESLint.Linter(); - linter.defineRule('tslint/config', rule); - linter.defineParser('@typescript-eslint/parser', parser); - - expect(() => linter.verify(code, config)).toThrow( - 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', - ); - } - - it('should error on missing project', () => { - testOutput('foo;', { - rules: { - 'tslint/config': [2, tslintRulesConfig], - }, - parser: '@typescript-eslint/parser', +if (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'true') { + describe('tslint/error', () => { + function testOutput(code: string, config: ClassicConfig.Config): void { + const linter = new TSESLint.Linter(); + linter.defineRule('tslint/config', rule); + linter.defineParser('@typescript-eslint/parser', parser); + + expect(() => linter.verify(code, config)).toThrow( + 'You have used a rule which requires parserServices to be generated. You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser.', + ); + } + + it('should error on missing project', () => { + testOutput('foo;', { + rules: { + 'tslint/config': [2, tslintRulesConfig], + }, + parser: '@typescript-eslint/parser', + }); }); - }); - it('should error on default parser', () => { - testOutput('foo;', { - parserOptions: { - project: TEST_PROJECT_PATH, - }, - rules: { - 'tslint/config': [2, tslintRulesConfig], - }, + it('should error on default parser', () => { + testOutput('foo;', { + parserOptions: { + project: TEST_PROJECT_PATH, + }, + rules: { + 'tslint/config': [2, tslintRulesConfig], + }, + }); }); - }); - it('should not crash if there are no tslint rules specified', () => { - const linter = new TSESLint.Linter(); - jest.spyOn(console, 'warn').mockImplementation(); - linter.defineRule('tslint/config', rule); - linter.defineParser('@typescript-eslint/parser', parser); - - const filePath = path.resolve( - __dirname, - 'fixtures', - 'test-project', - 'extra.ts', - ); - - expect(() => - linter.verify( - 'foo;', - { - parserOptions: { - project: TEST_PROJECT_PATH, - }, - rules: { - 'tslint/config': [2, {}], + it('should not crash if there are no tslint rules specified', () => { + const linter = new TSESLint.Linter(); + jest.spyOn(console, 'warn').mockImplementation(); + linter.defineRule('tslint/config', rule); + linter.defineParser('@typescript-eslint/parser', parser); + + const filePath = path.resolve( + __dirname, + 'fixtures', + 'test-project', + 'extra.ts', + ); + + expect(() => + linter.verify( + 'foo;', + { + parserOptions: { + project: TEST_PROJECT_PATH, + }, + rules: { + 'tslint/config': [2, {}], + }, + parser: '@typescript-eslint/parser', }, - parser: '@typescript-eslint/parser', - }, - filePath, - ), - ).not.toThrow(); - - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining( - `Tried to lint ${filePath} but found no valid, enabled rules for this file type and file path in the resolved configuration.`, - ), - ); - jest.resetAllMocks(); + filePath, + ), + ).not.toThrow(); + + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining( + `Tried to lint ${filePath} but found no valid, enabled rules for this file type and file path in the resolved configuration.`, + ), + ); + jest.resetAllMocks(); + }); }); -}); +} diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 52d2964594f0..a8c63c4c4ee3 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -57,6 +57,7 @@ "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 380dec18f4cc..1cf7f9d82992 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ import type * as ts from 'typescript/lib/tsserverlibrary'; -// eslint-disable-next-line @typescript-eslint/no-empty-function +import type { ProjectServiceOptions } from '../parser-options'; + const doNothing = (): void => {}; const createStubFileWatcher = (): ts.FileWatcher => ({ @@ -9,9 +11,15 @@ const createStubFileWatcher = (): ts.FileWatcher => ({ export type TypeScriptProjectService = ts.server.ProjectService; +export interface ProjectServiceSettings { + allowDefaultProjectForFiles: string[] | undefined; + service: TypeScriptProjectService; +} + export function createProjectService( - jsDocParsingMode?: ts.JSDocParsingMode, -): TypeScriptProjectService { + options: boolean | ProjectServiceOptions | undefined, + jsDocParsingMode: ts.JSDocParsingMode | undefined, +): ProjectServiceSettings { // We import this lazily to avoid its cost for users who don't use the service // TODO: Once we drop support for TS<5.3 we can import from "typescript" directly const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts; @@ -30,7 +38,7 @@ export function createProjectService( watchFile: createStubFileWatcher, }; - return new tsserver.server.ProjectService({ + const service = new tsserver.server.ProjectService({ host: system, cancellationToken: { isCancellationRequested: (): boolean => false }, useSingleInferredProject: false, @@ -49,4 +57,12 @@ export function createProjectService( session: undefined, jsDocParsingMode, }); + + return { + allowDefaultProjectForFiles: + typeof options === 'object' + ? options.allowDefaultProjectForFiles + : undefined, + service, + }; } diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 6b3429571014..a30942aa0d5b 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,7 +1,7 @@ import debug from 'debug'; import * as ts from 'typescript'; -import type { TypeScriptProjectService } from '../create-program/createProjectService'; +import type { ProjectServiceSettings } from '../create-program/createProjectService'; import { createProjectService } from '../create-program/createProjectService'; import { ensureAbsolutePath } from '../create-program/shared'; import type { TSESTreeOptions } from '../parser-options'; @@ -21,7 +21,7 @@ const log = debug( ); let TSCONFIG_MATCH_CACHE: ExpiringCache | null; -let TSSERVER_PROJECT_SERVICE: TypeScriptProjectService | null = null; +let TSSERVER_PROJECT_SERVICE: ProjectServiceSettings | null = null; // NOTE - we intentionally use "unnecessary" `?.` here because in TS<5.3 this enum doesn't exist // This object exists so we can centralize these for tracking and so we don't proliferate these across the file @@ -80,11 +80,14 @@ export function createParseSettings( errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: options.errorOnUnknownASTType === true, EXPERIMENTAL_projectService: - (options.EXPERIMENTAL_useProjectService === true && + (options.EXPERIMENTAL_useProjectService && process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER !== 'false') || (process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' && options.EXPERIMENTAL_useProjectService !== false) - ? (TSSERVER_PROJECT_SERVICE ??= createProjectService(jsDocParsingMode)) + ? (TSSERVER_PROJECT_SERVICE ??= createProjectService( + options.EXPERIMENTAL_useProjectService, + jsDocParsingMode, + )) : undefined, EXPERIMENTAL_useSourceOfProjectReferenceRedirect: options.EXPERIMENTAL_useSourceOfProjectReferenceRedirect === true, diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 352798be158f..6aca7a863aae 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -1,6 +1,6 @@ import type * as ts from 'typescript'; -import type * as tsserverlibrary from 'typescript/lib/tsserverlibrary'; +import type { ProjectServiceSettings } from '../create-program/createProjectService'; import type { CanonicalPath } from '../create-program/shared'; import type { TSESTree } from '../ts-estree'; import type { CacheLike } from './ExpiringCache'; @@ -67,9 +67,7 @@ export interface MutableParseSettings { /** * Experimental: TypeScript server to power program creation. */ - EXPERIMENTAL_projectService: - | tsserverlibrary.server.ProjectService - | undefined; + EXPERIMENTAL_projectService: ProjectServiceSettings | 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 badd40ae8cfa..e86be6d50996 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -101,6 +101,16 @@ interface ParseOptions { suppressDeprecatedPropertyWarnings?: boolean; } +/** + * Granular options to configure the project service. + */ +export interface ProjectServiceOptions { + /** + * Globs of files to allow running with the default inferred project settings. + */ + allowDefaultProjectForFiles?: string[]; +} + interface ParseAndGenerateServicesOptions extends ParseOptions { /** * Causes the parser to error if the TypeScript compiler returns any unexpected syntax/semantic errors. @@ -114,7 +124,7 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { * * @see https://github.com/typescript-eslint/typescript-eslint/issues/6575 */ - EXPERIMENTAL_useProjectService?: boolean; + EXPERIMENTAL_useProjectService?: boolean | ProjectServiceOptions; /** * ***EXPERIMENTAL FLAG*** - Use this at your own risk. diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index aeef8d1e45ff..08c29892e220 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -53,6 +53,7 @@ function getProgramAndAST( const fromProjectService = useProgramFromProjectService( parseSettings.EXPERIMENTAL_projectService, parseSettings, + hasFullTypeInformation, ); if (fromProjectService) { return fromProjectService; diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 59f6b0fe50ab..d49acd55e095 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,26 +1,44 @@ -import path from 'path'; -import type { server } from 'typescript/lib/tsserverlibrary'; +import { minimatch } from 'minimatch'; import { createProjectProgram } from './create-program/createProjectProgram'; -import { type ASTAndDefiniteProgram } from './create-program/shared'; +import type { ProjectServiceSettings } from './create-program/createProjectService'; +import { + type ASTAndDefiniteProgram, + ensureAbsolutePath, + getCanonicalFileName, +} from './create-program/shared'; import type { MutableParseSettings } from './parseSettings'; export function useProgramFromProjectService( - projectService: server.ProjectService, + { allowDefaultProjectForFiles, service }: ProjectServiceSettings, parseSettings: Readonly, + hasFullTypeInformation: boolean, ): ASTAndDefiniteProgram | undefined { - const opened = projectService.openClientFile( - absolutify(parseSettings.filePath), + const filePath = getCanonicalFileName(parseSettings.filePath); + + const opened = service.openClientFile( + ensureAbsolutePath(filePath, service.host.getCurrentDirectory()), parseSettings.codeFullText, /* scriptKind */ undefined, parseSettings.tsconfigRootDir, ); - if (!opened.configFileName) { - return undefined; + + if (hasFullTypeInformation) { + if (opened.configFileName) { + if (filePathMatchedBy(filePath, allowDefaultProjectForFiles)) { + throw new Error( + `${filePath} was included by allowDefaultProjectForFiles but also was found in the project service. Consider removing it from allowDefaultProjectForFiles.`, + ); + } + } else if (!filePathMatchedBy(filePath, allowDefaultProjectForFiles)) { + throw new Error( + `${filePath} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.`, + ); + } } - const scriptInfo = projectService.getScriptInfo(parseSettings.filePath); - const program = projectService + const scriptInfo = service.getScriptInfo(filePath); + const program = service .getDefaultProjectForFile(scriptInfo!.fileName, true)! .getLanguageService(/*ensureSynchronized*/ true) .getProgram(); @@ -30,10 +48,13 @@ export function useProgramFromProjectService( } return createProjectProgram(parseSettings, [program]); +} - function absolutify(filePath: string): string { - return path.isAbsolute(filePath) - ? filePath - : path.join(projectService.host.getCurrentDirectory(), filePath); - } +function filePathMatchedBy( + filePath: string, + allowDefaultProjectForFiles: string[] | undefined, +): boolean { + return !!allowDefaultProjectForFiles?.some(pattern => + minimatch(filePath, pattern), + ); } diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index f6f6d117e3bc..9541dcd43942 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -1,7 +1,21 @@ import { createProjectService } from '../../src/create-program/createProjectService'; describe('createProjectService', () => { - it('does not crash', () => { - createProjectService(); + it('sets allowDefaultProjectForFiles when options.allowDefaultProjectForFiles is defined', () => { + const allowDefaultProjectForFiles = ['./*.js']; + const settings = createProjectService( + { allowDefaultProjectForFiles }, + undefined, + ); + + expect(settings.allowDefaultProjectForFiles).toBe( + allowDefaultProjectForFiles, + ); + }); + + it('does not set allowDefaultProjectForFiles when options.allowDefaultProjectForFiles is not defined', () => { + const settings = createProjectService(undefined, undefined); + + expect(settings.allowDefaultProjectForFiles).toBeUndefined(); }); }); diff --git a/packages/typescript-estree/tests/lib/semanticInfo.test.ts b/packages/typescript-estree/tests/lib/semanticInfo.test.ts index 8502e70180c4..d580249079ab 100644 --- a/packages/typescript-estree/tests/lib/semanticInfo.test.ts +++ b/packages/typescript-estree/tests/lib/semanticInfo.test.ts @@ -337,7 +337,24 @@ describe('semanticInfo', () => { }); } - it('file not in provided program instance(s)', () => { + it('file not in single provided program instance should throw', () => { + const filename = 'non-existent-file.ts'; + const program = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); + const options = createOptions(filename); + const optionsWithSingleProgram = { + ...options, + programs: [program], + }; + expect(() => + parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram), + ).toThrow( + process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' + ? `${filename} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.` + : `The file was not found in any of the provided program instance(s): ${filename}`, + ); + }); + + it('file not in multiple provided program instances should throw a program instance error', () => { const filename = 'non-existent-file.ts'; const program1 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); const options = createOptions(filename); @@ -348,7 +365,9 @@ describe('semanticInfo', () => { expect(() => parseAndGenerateServices('const foo = 5;', optionsWithSingleProgram), ).toThrow( - `The file was not found in any of the provided program instance(s): ${filename}`, + process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' + ? `${filename} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.` + : `The file was not found in any of the provided program instance(s): ${filename}`, ); const program2 = createProgram(path.join(FIXTURES_DIR, 'tsconfig.json')); @@ -359,7 +378,9 @@ describe('semanticInfo', () => { expect(() => parseAndGenerateServices('const foo = 5;', optionsWithMultiplePrograms), ).toThrow( - `The file was not found in any of the provided program instance(s): ${filename}`, + process.env.TYPESCRIPT_ESLINT_EXPERIMENTAL_TSSERVER === 'true' + ? `${filename} was not found by the project service. Consider either including it in the tsconfig.json or including it in allowDefaultProjectForFiles.` + : `The file was not found in any of the provided program instance(s): ${filename}`, ); }); diff --git a/yarn.lock b/yarn.lock index d6dfd1b88c95..c0c163e1df6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6130,6 +6130,7 @@ __metadata: jest: 29.7.0 jest-specific-snapshot: ^8.0.0 make-dir: "*" + minimatch: 9.0.3 prettier: ^3.0.3 rimraf: "*" semver: ^7.5.4 @@ -14845,6 +14846,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:~9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -14881,15 +14891,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:~9.0.3": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" - dependencies: - brace-expansion: ^2.0.1 - checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 - languageName: node - linkType: hard - "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" 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