From 318f56d64b8c171891bf2d40cc878738cc266a6a Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Mon, 16 Sep 2024 14:39:16 +0800 Subject: [PATCH] don't treat client files as root in configured service so it can be removed from the service --- .../src/plugins/typescript/service.ts | 42 +++++++------- .../test/plugins/typescript/service.test.ts | 55 ++++++++++++++++++- .../different-ts-service/tsconfig.json | 1 + 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 023b7ff46..d0c7776f0 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -113,7 +113,6 @@ const dependedConfigWatchers = new FileMap(); const configPathToDependedProject = new FileMap(); const configFileModifiedTime = new FileMap(); const configFileForOpenFiles = new FileMap(); -const containingServices = new FileMap(); const pendingReloads = new FileSet(); const documentRegistries = new Map(); const pendingForAllServices = new Set>(); @@ -172,24 +171,19 @@ export async function getService( * Prevent infinite loop when the project reference is circular */ const triedTsConfig = new Set(); - const possibleConfigPath = new Set(); const needAssign = !configFileForOpenFiles.has(path); let service = await getConfiguredService(tsconfigPath); if (!needAssign) { return service; } - const defaultService = await findDefaultServiceForFile( - service, - triedTsConfig, - possibleConfigPath - ); + const defaultService = await findDefaultServiceForFile(service, triedTsConfig); if (defaultService) { configFileForOpenFiles.set(path, defaultService.tsconfigPath); return defaultService; } - for (const configPath of possibleConfigPath) { + for (const configPath of triedTsConfig) { const service = await getConfiguredService(configPath); const ls = service.getService(); if (ls.getProgram()?.getSourceFile(path)) { @@ -225,8 +219,7 @@ export async function getService( async function findDefaultServiceForFile( service: LanguageServiceContainer, - triedTsConfig: Set, - possibleConfigPath: Set + triedTsConfig: Set ): Promise { service.ensureProjectFileUpdates(); if (service.snapshotManager.isProjectFile(path)) { @@ -236,16 +229,15 @@ export async function getService( return; } - possibleConfigPath.add(service.tsconfigPath); + triedTsConfig.add(service.tsconfigPath); // TODO: maybe add support for ts 5.6's ancestor searching - return findDefaultFromProjectReferences(service, triedTsConfig, possibleConfigPath); + return findDefaultFromProjectReferences(service, triedTsConfig); } async function findDefaultFromProjectReferences( service: LanguageServiceContainer, - triedTsConfig: Set, - possibleConfigPath: Set + triedTsConfig: Set ) { const projectReferences = service.getResolvedProjectReferences(); if (projectReferences.length === 0) { @@ -265,11 +257,7 @@ export async function getService( for (const ref of possibleSubPaths) { const subService = await getConfiguredService(ref); - const defaultService = await findDefaultServiceForFile( - subService, - triedTsConfig, - possibleConfigPath - ); + const defaultService = await findDefaultServiceForFile(subService, triedTsConfig); if (defaultService) { return defaultService; } @@ -342,6 +330,7 @@ async function createLanguageService( const projectConfig = getParsedConfig(); const { options: compilerOptions, raw, errors: configErrors } = projectConfig; const allowJs = compilerOptions.allowJs ?? !!compilerOptions.checkJs; + const virtualDocuments = new FileMap(tsSystem.useCaseSensitiveFileNames); const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); watchWildCardDirectories(projectConfig); @@ -675,14 +664,22 @@ async function createLanguageService( : snapshotManager.getProjectFileNames(); const canonicalProjectFileNames = new Set(projectFiles.map(getCanonicalFileName)); + // We only assign project files or files already in the program to the language service + // so don't need to include other client files otherwise it will stay in the program and not be removed + const clientFiles = tsconfigPath + ? Array.from(virtualDocuments.values()) + .map((v) => v.getFilePath()) + .filter(isNotNullOrUndefined) + : snapshotManager.getClientFileNames(); + return Array.from( new Set([ ...projectFiles, // project file is read from the file system so it's more likely to have // the correct casing - ...snapshotManager - .getClientFileNames() - .filter((file) => !canonicalProjectFileNames.has(getCanonicalFileName(file))), + ...clientFiles.filter( + (file) => !canonicalProjectFileNames.has(getCanonicalFileName(file)) + ), ...svelteTsxFiles ]) ); @@ -1188,6 +1185,7 @@ async function createLanguageService( if (!filePath) { return; } + virtualDocuments.set(filePath, document); configFileForOpenFiles.set(filePath, tsconfigPath || workspacePath); updateSnapshot(document); scheduleUpdate(filePath); diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index ae2f0ff25..71db7f064 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -604,10 +604,63 @@ describe('service', () => { sinon.assert.calledWith(watchDirectory.firstCall, []); }); + it('assigns files to service with the file in the program', async () => { + const dirPath = getRandomVirtualDirPath(testDir); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + + const tsconfigPath = path.join(dirPath, 'tsconfig.json'); + virtualSystem.writeFile( + tsconfigPath, + JSON.stringify({ + compilerOptions: { + noImplicitOverride: true + }, + include: ['src/*.ts'] + }) + ); + + const referencedFile = path.join(dirPath, 'anotherPackage', 'index.svelte'); + const tsFilePath = path.join(dirPath, 'src', 'random.ts'); + + virtualSystem.readDirectory = () => [tsFilePath]; + virtualSystem.writeFile( + referencedFile, + '' + ); + virtualSystem.writeFile(tsFilePath, 'import "../anotherPackage/index.svelte";'); + + const document = new Document( + pathToUrl(referencedFile), + virtualSystem.readFile(referencedFile)! + ); + document.openedByClient = true; + const ls = await getService(referencedFile, rootUris, lsDocumentContext); + ls.updateSnapshot(document); + + assert.equal(normalizePath(ls.tsconfigPath), normalizePath(tsconfigPath)); + + const noImplicitOverrideErrorCode = 4114; + const findError = (ls: LanguageServiceContainer) => + ls + .getService() + .getSemanticDiagnostics(referencedFile) + .find((f) => f.code === noImplicitOverrideErrorCode); + + assert.ok(findError(ls)); + + virtualSystem.writeFile(tsFilePath, ''); + ls.updateTsOrJsFile(tsFilePath); + + const ls2 = await getService(referencedFile, rootUris, lsDocumentContext); + ls2.updateSnapshot(document); + + assert.deepStrictEqual(findError(ls2), undefined); + }); + function getSemanticDiagnosticsMessages(ls: LanguageServiceContainer, filePath: string) { return ls .getService() .getSemanticDiagnostics(filePath) - .map((d) => d.messageText); + .map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n')); } }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/different-ts-service/tsconfig.json b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/different-ts-service/tsconfig.json index 6d3385d79..04d6ed3d5 100644 --- a/packages/language-server/test/plugins/typescript/testfiles/diagnostics/different-ts-service/tsconfig.json +++ b/packages/language-server/test/plugins/typescript/testfiles/diagnostics/different-ts-service/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "allowJs": true, /** This is actually not needed, but makes the tests faster because TS does not look up other types.