Skip to content

Eagerly resolve module specifiers for auto-import completions in --moduleResolution node12+ #48752

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

Merged
merged 9 commits into from
Apr 27, 2022

Conversation

andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Apr 18, 2022

For auto-import completions, we used to not resolve the module specifier until the subsequent completion details request, triggered for one completion entry at a time when you focus it in the list. Later, we added a cache for module specifiers and started resolving up to 100 module specifiers per completions request, and pulling up to an additional 1000 from the cache.

Under --moduleResolution node12 or nodenext, we have to resolve module specifiers for auto-import completions right away, in excess of 100 if needed, because respecting package.json exports means that there may be exported symbols for which we cannot generate a module specifier without writing a relative path into "../node_modules/". We would prefer to treat such exports as unreachable and filter them from the list, which means we need to know the module specifier when generating the list. This is unfortunate for performance reasons if the list is very big, but we have no choice if we want to avoid showing completions for unreachable exports.

The introduction of the PackageJsonInfoCache in module resolution (added in the initial support for node12 and nodenext), and sharing that cache with the auto-imports logic (#47388) should mitigate some of this cost. I currently have no realistic project to measure this change with, as real-world projects using these yet-experimental resolution modes are rare or possibly nonexistent. I’ve added some bit flags to CompletionInfo that indicate a number of perf-impacting things that happened while computing the response, including the scenario that wasn’t possible before this PR, resolving >100 module specifiers in a single request.

@typescript-bot
Copy link
Collaborator

Thanks for the PR! It looks like you've changed the TSServer protocol in some way. Please ensure that any changes here don't break consumers of the current TSServer API. For some extra review, we'll ping @sheetalkamat, @amcasey, @mjbvz, @minestarks for you. Feel free to loop in other consumers/maintainers if necessary

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Apr 18, 2022
@@ -8463,7 +8463,7 @@ namespace ts {
export interface ResolvedModuleSpecifierInfo {
modulePaths: readonly ModulePath[] | undefined;
moduleSpecifiers: readonly string[] | undefined;
isAutoImportable: boolean | undefined;
isBlockedByPackageJsonDependencies: boolean | undefined;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a behavior change, but the property became a misnomer because in nodenext, factors that don't contribute to this property can make the module specifiers not auto-importable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Blocked" sounds like it means "not auto-importable". Is that correct and, if so, does that mean that the meaning of the flag has flipped?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For other reviewers, yes, the meaning has flipped.

@@ -629,19 +630,19 @@ namespace ts.codefix {
const importedSymbolHasValueMeaning = !!(exportInfo.targetFlags & SymbolFlags.Value);
const addAsTypeOnly = getAddAsTypeOnly(isValidTypeOnlyUseSite, /*isForNewImportDeclaration*/ true, exportInfo.symbol, exportInfo.targetFlags, checker, compilerOptions);
computedWithoutCacheCount += computedWithoutCache ? 1 : 0;
return moduleSpecifiers?.map((moduleSpecifier): FixAddNewImport | FixAddJsdocTypeImport =>
return mapDefined(moduleSpecifiers, (moduleSpecifier): FixAddNewImport | FixAddJsdocTypeImport | undefined =>
rejectNodeModulesRelativePaths && pathContainsNodeModules(moduleSpecifier) ? undefined :
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is the most important change.

}
const shouldResolveModuleSpecifier = isForImportStatementCompletion || preferences.allowIncompleteCompletions && resolvedCount < moduleSpecifierResolutionLimit;
const shouldResolveModuleSpecifier = needsFullResolution || preferences.allowIncompleteCompletions && resolvedCount < moduleSpecifierResolutionLimit;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is the next most important change. Everything else is bookkeeping, telemetry, renaming for accuracy, etc.

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to have to be a little careful that this doesn't get consumed by the telemetry system as a floating point number that might get rounded.

@@ -8463,7 +8463,7 @@ namespace ts {
export interface ResolvedModuleSpecifierInfo {
modulePaths: readonly ModulePath[] | undefined;
moduleSpecifiers: readonly string[] | undefined;
isAutoImportable: boolean | undefined;
isBlockedByPackageJsonDependencies: boolean | undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Blocked" sounds like it means "not auto-importable". Is that correct and, if so, does that mean that the meaning of the flag has flipped?

@@ -8463,7 +8463,7 @@ namespace ts {
export interface ResolvedModuleSpecifierInfo {
modulePaths: readonly ModulePath[] | undefined;
moduleSpecifiers: readonly string[] | undefined;
isAutoImportable: boolean | undefined;
isBlockedByPackageJsonDependencies: boolean | undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For other reviewers, yes, the meaning has flipped.

// package.json exports can mean we *can't* resolve a module specifier (that doesn't include a
// relative path into node_modules), and we want to filter those completions out entirely.
// Import statement completions always need specifier resolution because the module specifier is
// part of their `insertText`, not the `codeActions` creating edits away from the cursor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"away from the cursor" => "elsewhere in the file"?

interface CompletionInfo {
/** For performance telemetry. */
flags?: CompletionInfoFlags;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had discussed the possibility that this might subsume one or more existing flags, but it looks like that wasn't necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a property that we could delete in the future if we’re not worried about old clients using it.

@@ -6,6 +6,7 @@
"name": "1"
},
"completionList": {
"flags": 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skimming, I couldn't find one that wasn't 0?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem like any of the baselines have a value other than 0, no, but there is a unit test that has one of the flags set.

@andrewbranch andrewbranch requested review from sandersn and jakebailey and removed request for gabritto April 27, 2022 17:30
Copy link
Member

@jakebailey jakebailey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I'm not sure if you were wanting some tests that test all of the different flag code paths or not.

@andrewbranch
Copy link
Member Author

I wasn’t particularly concerned with testing all the telemetry flag combinations, was satisfied to see one of them come through in that unit test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

4 participants