mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Handle project / program roots in tsserver correctly (#58562)
This commit is contained in:
@@ -2893,7 +2893,7 @@ export class ProjectService {
|
||||
path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName);
|
||||
const existingValue = projectRootFilesMap.get(path);
|
||||
if (existingValue) {
|
||||
if (existingValue.info) {
|
||||
if (existingValue.info?.path === path) {
|
||||
project.removeFile(existingValue.info, /*fileExists*/ false, /*detachFromProject*/ true);
|
||||
existingValue.info = undefined;
|
||||
}
|
||||
|
||||
+20
-18
@@ -88,7 +88,6 @@ import {
|
||||
noopFileWatcher,
|
||||
normalizePath,
|
||||
normalizeSlashes,
|
||||
orderedRemoveItem,
|
||||
PackageJsonAutoImportPreference,
|
||||
PackageJsonInfo,
|
||||
ParsedCommandLine,
|
||||
@@ -309,8 +308,7 @@ const enum TypingWatcherType {
|
||||
type TypingWatchers = Map<Path, FileWatcher> & { isInvoked?: boolean; };
|
||||
|
||||
export abstract class Project implements LanguageServiceHost, ModuleResolutionHost {
|
||||
private rootFiles: ScriptInfo[] = [];
|
||||
private rootFilesMap = new Map<string, ProjectRootFile>();
|
||||
private rootFilesMap = new Map<Path, ProjectRootFile>();
|
||||
private program: Program | undefined;
|
||||
private externalFiles: SortedReadonlyArray<string> | undefined;
|
||||
private missingFilesMap: Map<Path, FileWatcher> | undefined;
|
||||
@@ -641,7 +639,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
}
|
||||
|
||||
getScriptFileNames() {
|
||||
if (!this.rootFiles) {
|
||||
if (!this.rootFilesMap.size) {
|
||||
return ts.emptyArray;
|
||||
}
|
||||
|
||||
@@ -667,7 +665,6 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
const existingValue = this.rootFilesMap.get(scriptInfo.path);
|
||||
if (existingValue && existingValue.info !== scriptInfo) {
|
||||
// This was missing path earlier but now the file exists. Update the root
|
||||
this.rootFiles.push(scriptInfo);
|
||||
existingValue.info = scriptInfo;
|
||||
}
|
||||
scriptInfo.attachToProject(this);
|
||||
@@ -1079,12 +1076,9 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
// Release external files
|
||||
forEach(this.externalFiles, externalFile => this.detachScriptInfoIfNotRoot(externalFile));
|
||||
// Always remove root files from the project
|
||||
for (const root of this.rootFiles) {
|
||||
root.detachFromProject(this);
|
||||
}
|
||||
this.rootFilesMap.forEach(root => root.info?.detachFromProject(this));
|
||||
this.projectService.pendingEnsureProjectForOpenFiles = true;
|
||||
|
||||
this.rootFiles = undefined!;
|
||||
this.rootFilesMap = undefined!;
|
||||
this.externalFiles = undefined;
|
||||
this.program = undefined;
|
||||
@@ -1135,11 +1129,11 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
}
|
||||
|
||||
isClosed() {
|
||||
return this.rootFiles === undefined;
|
||||
return this.rootFilesMap === undefined;
|
||||
}
|
||||
|
||||
hasRoots() {
|
||||
return this.rootFiles && this.rootFiles.length > 0;
|
||||
return !!this.rootFilesMap?.size;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -1147,8 +1141,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
return false;
|
||||
}
|
||||
|
||||
getRootFiles() {
|
||||
return this.rootFiles && this.rootFiles.map(info => info.fileName);
|
||||
getRootFiles(): NormalizedPath[] {
|
||||
return this.rootFilesMap && arrayFrom(ts.mapDefinedIterator(this.rootFilesMap.values(), value => value.info?.fileName));
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -1157,13 +1151,13 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
}
|
||||
|
||||
getRootScriptInfos() {
|
||||
return this.rootFiles;
|
||||
return arrayFrom(ts.mapDefinedIterator(this.rootFilesMap.values(), value => value.info));
|
||||
}
|
||||
|
||||
getScriptInfos(): ScriptInfo[] {
|
||||
if (!this.languageServiceEnabled) {
|
||||
// if language service is not enabled - return just root files
|
||||
return this.rootFiles;
|
||||
return this.getRootScriptInfos();
|
||||
}
|
||||
return map(this.program!.getSourceFiles(), sourceFile => {
|
||||
const scriptInfo = this.projectService.getScriptInfoForPath(sourceFile.resolvedPath);
|
||||
@@ -1256,13 +1250,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
}
|
||||
|
||||
isRoot(info: ScriptInfo) {
|
||||
return this.rootFilesMap && this.rootFilesMap.get(info.path)?.info === info;
|
||||
return this.rootFilesMap?.get(info.path)?.info === info;
|
||||
}
|
||||
|
||||
// add a root file to project
|
||||
addRoot(info: ScriptInfo, fileName?: NormalizedPath) {
|
||||
Debug.assert(!this.isRoot(info));
|
||||
this.rootFiles.push(info);
|
||||
this.rootFilesMap.set(info.path, { fileName: fileName || info.fileName, info });
|
||||
info.attachToProject(this);
|
||||
|
||||
@@ -1586,6 +1579,16 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
});
|
||||
}
|
||||
|
||||
// Update roots
|
||||
this.rootFilesMap.forEach((value, path) => {
|
||||
const file = this.program!.getSourceFileByPath(path);
|
||||
const info = value.info;
|
||||
if (!file || value.info?.path === file.resolvedPath) return;
|
||||
value.info = this.projectService.getScriptInfo(file.fileName)!;
|
||||
Debug.assert(value.info.isAttached(this));
|
||||
info?.detachFromProject(this);
|
||||
});
|
||||
|
||||
// Update the missing file paths watcher
|
||||
updateMissingFilePathsWatch(
|
||||
this.program,
|
||||
@@ -2006,7 +2009,6 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
|
||||
|
||||
// remove a root file from project
|
||||
protected removeRoot(info: ScriptInfo): void {
|
||||
orderedRemoveItem(this.rootFiles, info);
|
||||
this.rootFilesMap.delete(info.path);
|
||||
}
|
||||
|
||||
|
||||
@@ -217,10 +217,7 @@ describe("unittests:: tsserver:: dynamicFiles:: ", () => {
|
||||
}], session);
|
||||
}
|
||||
catch (e) {
|
||||
assert.strictEqual(
|
||||
e.message.replace(/\r?\n/, "\n"),
|
||||
`Debug Failure. False expression.\nVerbose Debug Information: {"fileName":"^walkThroughSnippet:/Users/UserName/projects/someProject/out/someFile#1.js","currentDirectory":"/user/username/projects/myproject","hostCurrentDirectory":"/","openKeys":[]}\nDynamic files must always be opened with service's current directory or service should support inferred project per projectRootPath.`,
|
||||
);
|
||||
session.logger.info(e.message);
|
||||
}
|
||||
const file2Path = file.path.replace("#1", "#2");
|
||||
openFilesForSession([{ file: file2Path, content: file.content }], session);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as ts from "../../_namespaces/ts.js";
|
||||
import { dedent } from "../../_namespaces/Utils.js";
|
||||
import { jsonToReadableText } from "../helpers.js";
|
||||
import { libContent } from "../helpers/contents.js";
|
||||
import { solutionBuildWithBaseline } from "../helpers/solutionBuilder.js";
|
||||
import {
|
||||
baselineTsserverLogs,
|
||||
@@ -1930,4 +1931,56 @@ const b: B = new B();`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("with dts file next to ts file", () => {
|
||||
const indexDts: File = {
|
||||
path: "/home/src/projects/project/src/index.d.ts",
|
||||
content: dedent`
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: unknown
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
const host = createServerHost({
|
||||
[indexDts.path]: indexDts.content,
|
||||
"/home/src/projects/project/src/index.ts": dedent`
|
||||
const api = {}
|
||||
`,
|
||||
"/home/src/projects/project/tsconfig.json": jsonToReadableText({
|
||||
include: [
|
||||
"src/*.d.ts",
|
||||
],
|
||||
references: [{ path: "./tsconfig.node.json" }],
|
||||
}),
|
||||
"/home/src/projects/project/tsconfig.node.json": jsonToReadableText({
|
||||
include: ["src/**/*"],
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
},
|
||||
}),
|
||||
[libFile.path]: libContent,
|
||||
});
|
||||
const session = new TestSession(host);
|
||||
openFilesForSession([{ file: indexDts, projectRootPath: "/home/src/projects/project" }], session);
|
||||
session.executeCommandSeq<ts.server.protocol.DocumentHighlightsRequest>({
|
||||
command: ts.server.protocol.CommandTypes.DocumentHighlights,
|
||||
arguments: {
|
||||
...protocolFileLocationFromSubstring(indexDts, "global"),
|
||||
filesToSearch: ["/home/src/projects/project/src/index.d.ts"],
|
||||
},
|
||||
});
|
||||
session.executeCommandSeq<ts.server.protocol.EncodedSemanticClassificationsRequest>({
|
||||
command: ts.server.protocol.CommandTypes.EncodedSemanticClassificationsFull,
|
||||
arguments: {
|
||||
file: indexDts.path,
|
||||
start: 0,
|
||||
length: indexDts.content.length,
|
||||
format: "2020",
|
||||
},
|
||||
});
|
||||
baselineTsserverLogs("projectReferences", "with dts file next to ts file", session);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1416,7 +1416,7 @@ describe("unittests:: tsserver:: projects::", () => {
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
assert.isTrue(e.message.indexOf("Debug Failure. False expression: Found script Info still attached to project") === 0);
|
||||
session.logger.log(e.message);
|
||||
}
|
||||
baselineTsserverLogs("projects", "assert when removing project", session);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user