Skip to content

Commit 94a7352

Browse files
authored
fix: include files indirectly belonging to a project into correct project (#2488)
Fixes #2486 Fixes #2485
1 parent 73125a6 commit 94a7352

File tree

5 files changed

+194
-38
lines changed

5 files changed

+194
-38
lines changed

packages/language-server/src/plugins/typescript/DocumentSnapshot.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export namespace DocumentSnapshot {
109109
tsSystem: ts.System
110110
) {
111111
if (isSvelteFilePath(filePath)) {
112-
return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options);
112+
return DocumentSnapshot.fromSvelteFilePath(filePath, createDocument, options, tsSystem);
113113
} else {
114114
return DocumentSnapshot.fromNonSvelteFilePath(filePath, tsSystem);
115115
}
@@ -173,9 +173,10 @@ export namespace DocumentSnapshot {
173173
export function fromSvelteFilePath(
174174
filePath: string,
175175
createDocument: (filePath: string, text: string) => Document,
176-
options: SvelteSnapshotOptions
176+
options: SvelteSnapshotOptions,
177+
tsSystem: ts.System
177178
) {
178-
const originalText = ts.sys.readFile(filePath) ?? '';
179+
const originalText = tsSystem.readFile(filePath) ?? '';
179180
return fromDocument(createDocument(filePath, originalText), options);
180181
}
181182
}

packages/language-server/src/plugins/typescript/service.ts

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export interface TsConfigInfo {
101101
extendedConfigPaths?: Set<string>;
102102
}
103103

104+
enum TsconfigSvelteDiagnostics {
105+
NO_SVELTE_INPUT = 100_001
106+
}
107+
104108
const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; // 20 MB
105109
const services = new FileMap<Promise<LanguageServiceContainer>>();
106110
const serviceSizeMap = new FileMap<number>();
@@ -173,12 +177,23 @@ export async function getService(
173177
return service;
174178
}
175179

180+
// First try to find a service whose includes config matches our file
176181
const defaultService = await findDefaultServiceForFile(service, triedTsConfig);
177182
if (defaultService) {
178183
configFileForOpenFiles.set(path, defaultService.tsconfigPath);
179184
return defaultService;
180185
}
181186

187+
// If no such service found, see if the file is part of any existing service indirectly.
188+
// This can happen if the includes doesn't match the file but it was imported from one of the included files.
189+
for (const configPath of triedTsConfig) {
190+
const service = await getConfiguredService(configPath);
191+
const ls = service.getService();
192+
if (ls.getProgram()?.getSourceFile(path)) {
193+
return service;
194+
}
195+
}
196+
182197
tsconfigPath = '';
183198
}
184199

@@ -217,6 +232,8 @@ export async function getService(
217232
return;
218233
}
219234

235+
triedTsConfig.add(service.tsconfigPath);
236+
220237
// TODO: maybe add support for ts 5.6's ancestor searching
221238
return findDefaultFromProjectReferences(service, triedTsConfig);
222239
}
@@ -315,6 +332,8 @@ async function createLanguageService(
315332

316333
const projectConfig = getParsedConfig();
317334
const { options: compilerOptions, raw, errors: configErrors } = projectConfig;
335+
const allowJs = compilerOptions.allowJs ?? !!compilerOptions.checkJs;
336+
const virtualDocuments = new FileMap<Document>(tsSystem.useCaseSensitiveFileNames);
318337

319338
const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames);
320339
watchWildCardDirectories(projectConfig);
@@ -360,6 +379,7 @@ async function createLanguageService(
360379
let languageServiceReducedMode = false;
361380
let projectVersion = 0;
362381
let dirty = projectConfig.fileNames.length > 0;
382+
let skipSvelteInputCheck = !tsconfigPath;
363383

364384
const host: ts.LanguageServiceHost = {
365385
log: (message) => Logger.debug(`[ts] ${message}`),
@@ -529,12 +549,19 @@ async function createLanguageService(
529549
return prevSnapshot;
530550
}
531551

552+
const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig);
553+
532554
if (!prevSnapshot) {
533555
svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath);
556+
if (configFileForOpenFiles.get(filePath) === '' && services.size > 1) {
557+
configFileForOpenFiles.delete(filePath);
558+
}
559+
} else if (prevSnapshot.scriptKind !== newSnapshot.scriptKind && !allowJs) {
560+
// if allowJs is false, we need to invalid the cache so that js svelte files can be loaded through module resolution
561+
svelteModuleLoader.deleteFromModuleCache(filePath);
562+
configFileForOpenFiles.delete(filePath);
534563
}
535564

536-
const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig);
537-
538565
snapshotManager.set(filePath, newSnapshot);
539566

540567
return newSnapshot;
@@ -640,14 +667,22 @@ async function createLanguageService(
640667
: snapshotManager.getProjectFileNames();
641668
const canonicalProjectFileNames = new Set(projectFiles.map(getCanonicalFileName));
642669

670+
// We only assign project files (i.e. those found through includes config) and virtual files to getScriptFileNames.
671+
// We don't to include other client files otherwise they stay in the program and are never removed
672+
const clientFiles = tsconfigPath
673+
? Array.from(virtualDocuments.values())
674+
.map((v) => v.getFilePath())
675+
.filter(isNotNullOrUndefined)
676+
: snapshotManager.getClientFileNames();
677+
643678
return Array.from(
644679
new Set([
645680
...projectFiles,
646681
// project file is read from the file system so it's more likely to have
647682
// the correct casing
648-
...snapshotManager
649-
.getClientFileNames()
650-
.filter((file) => !canonicalProjectFileNames.has(getCanonicalFileName(file))),
683+
...clientFiles.filter(
684+
(file) => !canonicalProjectFileNames.has(getCanonicalFileName(file))
685+
),
651686
...svelteTsxFiles
652687
])
653688
);
@@ -736,20 +771,6 @@ async function createLanguageService(
736771
}
737772
}
738773

739-
const svelteConfigDiagnostics = checkSvelteInput(parsedConfig);
740-
if (svelteConfigDiagnostics.length > 0) {
741-
docContext.reportConfigError?.({
742-
uri: pathToUrl(tsconfigPath),
743-
diagnostics: svelteConfigDiagnostics.map((d) => ({
744-
message: d.messageText as string,
745-
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
746-
severity: ts.DiagnosticCategory.Error,
747-
source: 'svelte'
748-
}))
749-
});
750-
parsedConfig.errors.push(...svelteConfigDiagnostics);
751-
}
752-
753774
return {
754775
...parsedConfig,
755776
fileNames: parsedConfig.fileNames.map(normalizePath),
@@ -758,22 +779,32 @@ async function createLanguageService(
758779
};
759780
}
760781

761-
function checkSvelteInput(config: ts.ParsedCommandLine) {
782+
function checkSvelteInput(program: ts.Program | undefined, config: ts.ParsedCommandLine) {
762783
if (!tsconfigPath || config.raw.references || config.raw.files) {
763784
return [];
764785
}
765786

766-
const svelteFiles = config.fileNames.filter(isSvelteFilePath);
767-
if (svelteFiles.length > 0) {
787+
const configFileName = basename(tsconfigPath);
788+
// Only report to possible nearest config file since referenced project might not be a svelte project
789+
if (configFileName !== 'tsconfig.json' && configFileName !== 'jsconfig.json') {
790+
return [];
791+
}
792+
793+
const hasSvelteFiles =
794+
config.fileNames.some(isSvelteFilePath) ||
795+
program?.getSourceFiles().some((file) => isSvelteFilePath(file.fileName));
796+
797+
if (hasSvelteFiles) {
768798
return [];
769799
}
800+
770801
const { include, exclude } = config.raw;
771802
const inputText = JSON.stringify(include);
772803
const excludeText = JSON.stringify(exclude);
773804
const svelteConfigDiagnostics: ts.Diagnostic[] = [
774805
{
775806
category: ts.DiagnosticCategory.Error,
776-
code: 0,
807+
code: TsconfigSvelteDiagnostics.NO_SVELTE_INPUT,
777808
file: undefined,
778809
start: undefined,
779810
length: undefined,
@@ -933,6 +964,28 @@ async function createLanguageService(
933964
dirty = false;
934965
compilerHost = undefined;
935966

967+
if (!skipSvelteInputCheck) {
968+
const svelteConfigDiagnostics = checkSvelteInput(program, projectConfig);
969+
const codes = svelteConfigDiagnostics.map((d) => d.code);
970+
if (!svelteConfigDiagnostics.length) {
971+
// stop checking once it passed once
972+
skipSvelteInputCheck = true;
973+
}
974+
// report even if empty to clear previous diagnostics
975+
docContext.reportConfigError?.({
976+
uri: pathToUrl(tsconfigPath),
977+
diagnostics: svelteConfigDiagnostics.map((d) => ({
978+
message: d.messageText as string,
979+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
980+
severity: ts.DiagnosticCategory.Error,
981+
source: 'svelte'
982+
}))
983+
});
984+
projectConfig.errors = projectConfig.errors
985+
.filter((e) => !codes.includes(e.code))
986+
.concat(svelteConfigDiagnostics);
987+
}
988+
936989
// https://github.com/microsoft/TypeScript/blob/23faef92703556567ddbcb9afb893f4ba638fc20/src/server/project.ts#L1624
937990
// host.getCachedExportInfoMap will create the cache if it doesn't exist
938991
// so we need to check the property instead
@@ -1135,6 +1188,7 @@ async function createLanguageService(
11351188
if (!filePath) {
11361189
return;
11371190
}
1191+
virtualDocuments.set(filePath, document);
11381192
configFileForOpenFiles.set(filePath, tsconfigPath || workspacePath);
11391193
updateSnapshot(document);
11401194
scheduleUpdate(filePath);

packages/language-server/src/svelte-check.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,19 +209,14 @@ export class SvelteCheck {
209209
};
210210

211211
if (lsContainer.configErrors.length > 0) {
212-
const grouped = groupBy(
213-
lsContainer.configErrors,
214-
(error) => error.file?.fileName ?? tsconfigPath
215-
);
216-
217-
return Object.entries(grouped).map(([filePath, errors]) => ({
218-
filePath,
219-
text: '',
220-
diagnostics: errors.map((diagnostic) => map(diagnostic))
221-
}));
212+
return reportConfigError();
222213
}
223214

224215
const lang = lsContainer.getService();
216+
if (lsContainer.configErrors.length > 0) {
217+
return reportConfigError();
218+
}
219+
225220
const files = lang.getProgram()?.getSourceFiles() || [];
226221
const options = lang.getProgram()?.getCompilerOptions() || {};
227222

@@ -318,6 +313,19 @@ export class SvelteCheck {
318313
}
319314
})
320315
);
316+
317+
function reportConfigError() {
318+
const grouped = groupBy(
319+
lsContainer.configErrors,
320+
(error) => error.file?.fileName ?? tsconfigPath
321+
);
322+
323+
return Object.entries(grouped).map(([filePath, errors]) => ({
324+
filePath,
325+
text: '',
326+
diagnostics: errors.map((diagnostic) => map(diagnostic))
327+
}));
328+
}
321329
}
322330

323331
private async getDiagnosticsForFile(uri: string) {

packages/language-server/test/plugins/typescript/service.test.ts

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,46 @@ describe('service', () => {
108108
assert.ok(called);
109109
});
110110

111-
it('do not errors if referenced tsconfig matches no svelte files', async () => {
111+
it('does not report no svelte files when loaded through import', async () => {
112+
const dirPath = getRandomVirtualDirPath(testDir);
113+
const { virtualSystem, lsDocumentContext, rootUris } = setup();
114+
115+
virtualSystem.readDirectory = () => [path.join(dirPath, 'random.ts')];
116+
117+
virtualSystem.writeFile(
118+
path.join(dirPath, 'tsconfig.json'),
119+
JSON.stringify({
120+
include: ['**/*.ts']
121+
})
122+
);
123+
124+
virtualSystem.writeFile(
125+
path.join(dirPath, 'random.svelte'),
126+
'<script lang="ts">const a: number = null;</script>'
127+
);
128+
129+
virtualSystem.writeFile(
130+
path.join(dirPath, 'random.ts'),
131+
'import {} from "./random.svelte"'
132+
);
133+
134+
let called = false;
135+
const service = await getService(path.join(dirPath, 'random.svelte'), rootUris, {
136+
...lsDocumentContext,
137+
reportConfigError: (message) => {
138+
called = true;
139+
assert.deepStrictEqual([], message.diagnostics);
140+
}
141+
});
142+
143+
assert.equal(
144+
normalizePath(path.join(dirPath, 'tsconfig.json')),
145+
normalizePath(service.tsconfigPath)
146+
);
147+
assert.ok(called);
148+
});
149+
150+
it('does not errors if referenced tsconfig matches no svelte files', async () => {
112151
const dirPath = getRandomVirtualDirPath(testDir);
113152
const { virtualSystem, lsDocumentContext, rootUris } = setup();
114153

@@ -565,10 +604,63 @@ describe('service', () => {
565604
sinon.assert.calledWith(watchDirectory.firstCall, <RelativePattern[]>[]);
566605
});
567606

607+
it('assigns files to service with the file in the program', async () => {
608+
const dirPath = getRandomVirtualDirPath(testDir);
609+
const { virtualSystem, lsDocumentContext, rootUris } = setup();
610+
611+
const tsconfigPath = path.join(dirPath, 'tsconfig.json');
612+
virtualSystem.writeFile(
613+
tsconfigPath,
614+
JSON.stringify({
615+
compilerOptions: <ts.CompilerOptions>{
616+
noImplicitOverride: true
617+
},
618+
include: ['src/*.ts']
619+
})
620+
);
621+
622+
const referencedFile = path.join(dirPath, 'anotherPackage', 'index.svelte');
623+
const tsFilePath = path.join(dirPath, 'src', 'random.ts');
624+
625+
virtualSystem.readDirectory = () => [tsFilePath];
626+
virtualSystem.writeFile(
627+
referencedFile,
628+
'<script lang="ts">class A { a =1 }; class B extends A { a =2 };</script>'
629+
);
630+
virtualSystem.writeFile(tsFilePath, 'import "../anotherPackage/index.svelte";');
631+
632+
const document = new Document(
633+
pathToUrl(referencedFile),
634+
virtualSystem.readFile(referencedFile)!
635+
);
636+
document.openedByClient = true;
637+
const ls = await getService(referencedFile, rootUris, lsDocumentContext);
638+
ls.updateSnapshot(document);
639+
640+
assert.equal(normalizePath(ls.tsconfigPath), normalizePath(tsconfigPath));
641+
642+
const noImplicitOverrideErrorCode = 4114;
643+
const findError = (ls: LanguageServiceContainer) =>
644+
ls
645+
.getService()
646+
.getSemanticDiagnostics(referencedFile)
647+
.find((f) => f.code === noImplicitOverrideErrorCode);
648+
649+
assert.ok(findError(ls));
650+
651+
virtualSystem.writeFile(tsFilePath, '');
652+
ls.updateTsOrJsFile(tsFilePath);
653+
654+
const ls2 = await getService(referencedFile, rootUris, lsDocumentContext);
655+
ls2.updateSnapshot(document);
656+
657+
assert.deepStrictEqual(findError(ls2), undefined);
658+
});
659+
568660
function getSemanticDiagnosticsMessages(ls: LanguageServiceContainer, filePath: string) {
569661
return ls
570662
.getService()
571663
.getSemanticDiagnostics(filePath)
572-
.map((d) => d.messageText);
664+
.map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n'));
573665
}
574666
});

packages/language-server/test/plugins/typescript/testfiles/diagnostics/different-ts-service/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"compilerOptions": {
3+
"allowJs": true,
34
/**
45
This is actually not needed, but makes the tests faster
56
because TS does not look up other types.

0 commit comments

Comments
 (0)