Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Experiment] Some tweaking to handle project references for auto import #55955

Merged
merged 13 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9064,6 +9064,7 @@ export interface SymlinkCache {
getSymlinkedFiles(): ReadonlyMap<Path, string> | undefined;
setSymlinkedDirectory(symlink: string, real: SymlinkedDirectory | false): void;
setSymlinkedFile(symlinkPath: Path, real: string): void;
hasAnySymlinks(): boolean;
/**
* @internal
* Uses resolvedTypeReferenceDirectives from program instead of from files, since files
Expand Down Expand Up @@ -9118,8 +9119,13 @@ export function createSymlinkCache(cwd: string, getCanonicalFileName: GetCanonic
typeReferenceDirectives.forEach(resolution => processResolution(this, resolution.resolvedTypeReferenceDirective));
},
hasProcessedResolutions: () => hasProcessedResolutions,
hasAnySymlinks,
};

function hasAnySymlinks() {
return !!symlinkedFiles?.size || (!!symlinkedDirectories && !!forEachEntry(symlinkedDirectories, value => !!value));
}

function processResolution(cache: SymlinkCache, resolution: ResolvedModuleFull | ResolvedTypeReferenceDirective | undefined) {
if (!resolution || !resolution.originalPath || !resolution.resolvedFileName) return;
const { resolvedFileName, originalPath } = resolution;
Expand Down
2 changes: 1 addition & 1 deletion src/harness/tsserverLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function sanitizeLog(s: string): string {
s = s.replace(/getExportInfoMap: done in \d+(?:\.\d+)?/g, `getExportInfoMap: done in *`);
s = s.replace(/collectAutoImports: \d+(?:\.\d+)?/g, `collectAutoImports: *`);
s = s.replace(/continuePreviousIncompleteResponse: \d+(?:\.\d+)?/g, `continuePreviousIncompleteResponse: *`);
s = s.replace(/dependencies in \d+(?:\.\d+)?/g, `dependencies in *`);
s = s.replace(/referenced projects in \d+(?:\.\d+)?/g, `referenced projects in *`);
s = s.replace(/"exportMapKey":\s*"\d+ \d+ /g, match => match.replace(/ \d+ /, ` * `));
s = s.replace(/getIndentationAtPosition: getCurrentSourceFile: \d+(?:\.\d+)?/, `getIndentationAtPosition: getCurrentSourceFile: *`);
s = s.replace(/getIndentationAtPosition: computeIndentation\s*: \d+(?:\.\d+)?/, `getIndentationAtPosition: computeIndentation: *`);
Expand Down
14 changes: 13 additions & 1 deletion src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1698,6 +1698,13 @@ export class ProjectService {
const project = this.getConfiguredProjectByCanonicalConfigFilePath(projectCanonicalPath);
if (!project) return;

if (configuredProjectForConfig !== project) {
const path = this.toPath(configFileName);
if (find(project.getCurrentProgram()?.getResolvedProjectReferences(), ref => ref?.sourceFile.path === path)) {
project.markAutoImportProviderAsDirty();
}
}

// Load root file names for configured project with the config file name
// But only schedule update if project references this config file
const updateLevel = configuredProjectForConfig === project ? ProgramUpdateLevel.RootNamesAndUpdate : ProgramUpdateLevel.Update;
Expand Down Expand Up @@ -1763,11 +1770,16 @@ export class ProjectService {
project.pendingUpdateLevel = ProgramUpdateLevel.Full;
project.pendingUpdateReason = loadReason;
this.delayUpdateProjectGraph(project);
project.markAutoImportProviderAsDirty();
}
else {
// Change in referenced project config file
project.resolutionCache.removeResolutionsFromProjectReferenceRedirects(this.toPath(canonicalConfigFilePath));
const path = this.toPath(canonicalConfigFilePath);
project.resolutionCache.removeResolutionsFromProjectReferenceRedirects(path);
this.delayUpdateProjectGraph(project);
if (find(project.getCurrentProgram()?.getResolvedProjectReferences(), ref => ref?.sourceFile.path === path)) {
project.markAutoImportProviderAsDirty();
}
}
});
return scheduledAnyProjectUpdate;
Expand Down
57 changes: 38 additions & 19 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
comparePaths,
CompilerHost,
CompilerOptions,
concatenate,
containsPath,
createCacheableExportInfoMap,
createLanguageService,
Expand Down Expand Up @@ -1316,6 +1315,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}
}

/** @internal */
markAutoImportProviderAsDirty() {
if (!this.autoImportProviderHost) this.autoImportProviderHost = undefined;
this.autoImportProviderHost?.markAsDirty();
}

/** @internal */
onAutoImportProviderSettingsChanged() {
if (this.autoImportProviderHost === false) {
Expand Down Expand Up @@ -1400,8 +1405,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
this.projectProgramVersion++;
}
if (hasAddedorRemovedFiles) {
if (!this.autoImportProviderHost) this.autoImportProviderHost = undefined;
this.autoImportProviderHost?.markAsDirty();
this.markAutoImportProviderAsDirty();
}
if (isFirstProgramLoad) {
// Preload auto import provider so it's not created during completions request
Expand Down Expand Up @@ -2439,7 +2443,7 @@ export class AutoImportProviderProject extends Project {

const start = timestamp();
let dependencyNames: Set<string> | undefined;
let rootNames: string[] | undefined;
let rootNames: Set<string> | undefined;
const rootFileName = combinePaths(hostProject.currentDirectory, inferredTypesContainingFile);
const packageJsons = hostProject.getPackageJsonsForAutoImport(combinePaths(hostProject.currentDirectory, rootFileName));
for (const packageJson of packageJsons) {
Expand Down Expand Up @@ -2471,8 +2475,7 @@ export class AutoImportProviderProject extends Project {
if (packageJson) {
const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache);
if (entrypoints) {
rootNames = concatenate(rootNames, entrypoints);
dependenciesAdded += entrypoints.length ? 1 : 0;
dependenciesAdded += addRootNames(entrypoints);
continue;
}
}
Expand All @@ -2490,8 +2493,7 @@ export class AutoImportProviderProject extends Project {
);
if (typesPackageJson) {
const entrypoints = getRootNamesFromPackageJson(typesPackageJson, program, symlinkCache);
rootNames = concatenate(rootNames, entrypoints);
dependenciesAdded += entrypoints?.length ? 1 : 0;
dependenciesAdded += addRootNames(entrypoints);
return true;
}
}
Expand All @@ -2504,16 +2506,29 @@ export class AutoImportProviderProject extends Project {
// package and load the JS.
if (packageJson && compilerOptions.allowJs && compilerOptions.maxNodeModuleJsDepth) {
const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache, /*resolveJs*/ true);
rootNames = concatenate(rootNames, entrypoints);
dependenciesAdded += entrypoints?.length ? 1 : 0;
dependenciesAdded += addRootNames(entrypoints);
}
}
}

if (rootNames?.length) {
hostProject.log(`AutoImportProviderProject: found ${rootNames.length} root files in ${dependenciesAdded} dependencies in ${timestamp() - start} ms`);
const references = program.getResolvedProjectReferences();
let referencesAddded = 0;
if (references?.length && program.getCompilerOptions().paths) {
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
// Add direct referenced projects to rootFiles names
references.forEach(ref => referencesAddded += addRootNames(filterEntrypoints(ref?.commandLine.fileNames)));
}

if (rootNames?.size) {
hostProject.log(`AutoImportProviderProject: found ${rootNames.size} root files in ${dependenciesAdded} dependencies ${referencesAddded} referenced projects in ${timestamp() - start} ms`);
}
return rootNames ? arrayFrom(rootNames.values()) : ts.emptyArray;

function addRootNames(entrypoints: readonly string[] | undefined) {
if (!entrypoints?.length) return 0;
rootNames ??= new Set();
entrypoints.forEach(entry => rootNames!.add(entry));
return 1;
}
return rootNames || ts.emptyArray;

function addDependency(dependency: string) {
if (!startsWith(dependency, "@types/")) {
Expand All @@ -2540,14 +2555,18 @@ export class AutoImportProviderProject extends Project {
});
}

return mapDefined(entrypoints, entrypoint => {
const resolvedFileName = isSymlink ? entrypoint.replace(packageJson.packageDirectory, real!) : entrypoint;
if (!program.getSourceFile(resolvedFileName) && !(isSymlink && program.getSourceFile(entrypoint))) {
return resolvedFileName;
}
});
return filterEntrypoints(entrypoints, isSymlink ? entrypoint => entrypoint.replace(packageJson.packageDirectory, real!) : undefined);
}
}

function filterEntrypoints(entrypoints: readonly string[] | undefined, symlinkName?: (entrypoint: string) => string) {
return mapDefined(entrypoints, entrypoint => {
const resolvedFileName = symlinkName ? symlinkName(entrypoint) : entrypoint;
if (!program!.getSourceFile(resolvedFileName) && !(symlinkName && program!.getSourceFile(entrypoint))) {
return resolvedFileName;
}
});
}
}

/** @internal */
Expand Down
4 changes: 3 additions & 1 deletion src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3948,7 +3948,9 @@ function getCompletionData(
// If module transpilation is enabled or we're targeting es6 or above, or not emitting, OK.
if (compilerOptionsIndicateEsModules(program.getCompilerOptions())) return true;
// If some file is using ES6 modules, assume that it's OK to add more.
return programContainsModules(program);
return program.getSymlinkCache?.().hasAnySymlinks() ||
!!program.getCompilerOptions().paths ||
programContainsModules(program);
}

function isSnippetScope(scopeNode: Node): boolean {
Expand Down
182 changes: 182 additions & 0 deletions src/testRunner/unittests/tsserver/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,186 @@ export interface BrowserRouterProps {
session,
);
});

it("in project where there are no imports but has project references setup", () => {
const host = createServerHost({
"/user/username/projects/app/src/index.ts": "",
"/user/username/projects/app/tsconfig.json": JSON.stringify(
{
compilerOptions: { outDir: "dist", rootDir: "src" },
include: ["./src/**/*"],
references: [
{ path: "../shared" },
],
},
undefined,
" ",
),
"/user/username/projects/app/package.json": JSON.stringify(
{
name: "app",
version: "1.0.0",
main: "dist/index.js",
dependencies: {
shared: "1.0.0",
},
},
undefined,
" ",
),
"/user/username/projects/shared/src/index.ts": "export class MyClass { }",
"/user/username/projects/shared/tsconfig.json": JSON.stringify(
{
compilerOptions: { composite: true, outDir: "dist", rootDir: "src" },
include: ["./src/**/*"],
},
undefined,
" ",
),
"/user/username/projects/shared/package.json": JSON.stringify(
{
name: "shared",
version: "1.0.0",
main: "dist/index.js",
},
undefined,
" ",
),
"/user/username/projects/app/node_modules/shared": { symLink: "/user/username/projects/shared" },
});
const session = new TestSession(host);
session.executeCommandSeq<ts.server.protocol.ConfigureRequest>({
command: ts.server.protocol.CommandTypes.Configure,
arguments: {
preferences: {
includePackageJsonAutoImports: "auto",
},
},
});
openFilesForSession(["/user/username/projects/app/src/index.ts"], session);
session.executeCommandSeq<ts.server.protocol.CompletionsRequest>({
command: ts.server.protocol.CommandTypes.CompletionInfo,
arguments: {
file: "/user/username/projects/app/src/index.ts",
line: 1,
offset: 1,
includeExternalModuleExports: true,
includeInsertTextCompletions: true,
},
});
baselineTsserverLogs("completions", "in project where there are no imports but has project references setup", session);
});

describe("in project reference setup with path mapping sheetal", () => {
function completions(session: TestSession) {
session.executeCommandSeq<ts.server.protocol.CompletionsRequest>({
command: ts.server.protocol.CommandTypes.CompletionInfo,
arguments: {
file: "/user/username/projects/app/src/index.ts",
line: 1,
offset: 1,
includeExternalModuleExports: true,
includeInsertTextCompletions: true,
},
});
}
function verify(withExistingImport: boolean) {
it(`in project reference setup with path mapping${withExistingImport ? " with existing import" : ""}`, () => {
const host = createServerHost({
"/user/username/projects/app/src/index.ts": `

${withExistingImport ? "import { MyClass } from 'shared';" : ""}`,
"/user/username/projects/app/tsconfig.json": JSON.stringify(
{
compilerOptions: {
outDir: "dist",
rootDir: "src",
paths: {
"shared": ["./../shared/src/index.ts"],
"shared/*": ["./../shared/src/*.ts"],
},
},
include: ["./src/**/*"],
references: [
{ path: "../shared" },
],
},
undefined,
" ",
),
"/user/username/projects/app/package.json": JSON.stringify(
{
name: "app",
version: "1.0.0",
main: "dist/index.js",
dependencies: {
shared: "1.0.0",
},
},
undefined,
" ",
),
"/user/username/projects/shared/src/index.ts": "export class MyClass { }",
"/user/username/projects/shared/src/helper.ts": "export class MyHelper { }",
"/user/username/projects/shared/tsconfig.json": JSON.stringify(
{
compilerOptions: { composite: true, outDir: "dist", rootDir: "src" },
include: ["./src/**/*"],
references: [
{ path: "../mylib" },
],
},
undefined,
" ",
),
"/user/username/projects/shared/package.json": JSON.stringify(
{
name: "shared",
version: "1.0.0",
main: "dist/index.js",
},
undefined,
" ",
),
// Indirect ones should not be offered through auto import
"/user/username/projects/mylib/src/index.ts": "export class MyLibClass { }",
"/user/username/projects/mylib/tsconfig.json": JSON.stringify(
{
compilerOptions: { composite: true, outDir: "dist", rootDir: "src" },
include: ["./src/**/*"],
},
undefined,
" ",
),
"/user/username/projects/mylib/package.json": JSON.stringify(
{
name: "mylib",
version: "1.0.0",
main: "dist/index.js",
},
undefined,
" ",
),
});
const session = new TestSession(host);
session.executeCommandSeq<ts.server.protocol.ConfigureRequest>({
command: ts.server.protocol.CommandTypes.Configure,
arguments: {
preferences: {
includePackageJsonAutoImports: "auto",
},
},
});
openFilesForSession(["/user/username/projects/app/src/index.ts"], session);
completions(session);
host.writeFile("/user/username/projects/shared/src/other.ts", "export class OtherClass { }");
completions(session);
host.writeFile("/user/username/projects/mylib/src/otherlib.ts", "export class OtherLibClass { }");
completions(session);
baselineTsserverLogs("completions", `in project reference setup with path mapping${withExistingImport ? " with existing import" : ""}`, session);
});
}
verify(/*withExistingImport*/ true);
verify(/*withExistingImport*/ false);
});
});
Loading