Import statement completions (#43149)

* WIP

* WIP

* Get completion details working

* Start unifying eager and lazy auto imports

* Fix export=

* Fix completion details for totally misspelled names

* Almost fixed duplication...

* Fix remaining completion tests

* Refactor to support multiple origins for same symbol

* Make import fixes make slightly more sense

* Add cache back in

* Set insertText based on import kind

* Update API baselines

* Add semicolons, snippet support, and sourceDisplay

* Add some tests

* Update baselines

* Fix pattern ambient modules appearing in auto imports

* Fix tests

* Remove commented code

* Switch to valueDeclaration for getting module source file

* Small optimizations

* Cache module specifiers / importableness and export map separately

* Fix and test cache invalidation logic

* Update API baselines

* Add separate user preference for snippet-formatted completions

* Require first character to match when resolving module specifiers

* Fix AutoImportProvider export map cache invalidation

* Really fix auto import provider export map invalidation

* Update test added in master

* Use logical or assignment

Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>

* Simply conditional by reversing

Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>

* When file is deleted need to marked correctly in the project as removed file

* Simplify hasAddedOrRemovedSymlinks with cherry-picked fix

* Ensure replacement range is on one line

* Update baselines

Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>
Co-authored-by: Sheetal Nandi <shkamat@microsoft.com>
This commit is contained in:
Andrew Branch
2021-03-26 14:47:07 -07:00
committed by GitHub
co-authored by Daniel Rosenwasser Sheetal Nandi
parent a545ab1ac2
commit 2d6a490363
38 changed files with 1578 additions and 846 deletions
+13 -6
View File
@@ -3498,7 +3498,10 @@ namespace ts {
const exports = getExportsOfModuleAsArray(moduleSymbol);
const exportEquals = resolveExternalModuleSymbol(moduleSymbol);
if (exportEquals !== moduleSymbol) {
addRange(exports, getPropertiesOfType(getTypeOfSymbol(exportEquals)));
const type = getTypeOfSymbol(exportEquals);
if (shouldTreatPropertiesOfExternalModuleAsExports(type)) {
addRange(exports, getPropertiesOfType(type));
}
}
return exports;
}
@@ -3522,11 +3525,15 @@ namespace ts {
}
const type = getTypeOfSymbol(exportEquals);
return type.flags & TypeFlags.Primitive ||
getObjectFlags(type) & ObjectFlags.Class ||
isArrayOrTupleLikeType(type)
? undefined
: getPropertyOfType(type, memberName);
return shouldTreatPropertiesOfExternalModuleAsExports(type) ? getPropertyOfType(type, memberName) : undefined;
}
function shouldTreatPropertiesOfExternalModuleAsExports(resolvedExternalModuleType: Type) {
return !(resolvedExternalModuleType.flags & TypeFlags.Primitive ||
getObjectFlags(resolvedExternalModuleType) & ObjectFlags.Class ||
// `isArrayOrTupleLikeType` is too expensive to use in this auto-imports hot path
isArrayType(resolvedExternalModuleType) ||
isTupleType(resolvedExternalModuleType));
}
function getExportsOfSymbol(symbol: Symbol): SymbolTable {
+11 -9
View File
@@ -327,19 +327,17 @@ namespace ts.moduleSpecifiers {
: undefined);
}
interface ModulePath {
path: string;
isInNodeModules: boolean;
isRedirect: boolean;
}
/**
* Looks for existing imports that use symlinks to this module.
* Symlinks will be returned first so they are preferred over the real path.
*/
function getAllModulePaths(importingFileName: string, importedFileName: string, host: ModuleSpecifierResolutionHost): readonly ModulePath[] {
const cwd = host.getCurrentDirectory();
function getAllModulePaths(importingFileName: Path, importedFileName: string, host: ModuleSpecifierResolutionHost): readonly ModulePath[] {
const cache = host.getModuleSpecifierCache?.();
const getCanonicalFileName = hostGetCanonicalFileName(host);
if (cache) {
const cached = cache.get(importingFileName, toPath(importedFileName, host.getCurrentDirectory(), getCanonicalFileName));
if (typeof cached === "object") return cached;
}
const allFileNames = new Map<string, { path: string, isRedirect: boolean, isInNodeModules: boolean }>();
let importedFileFromNodeModules = false;
forEachFileNameOfModule(
@@ -358,7 +356,7 @@ namespace ts.moduleSpecifiers {
// Sort by paths closest to importing file Name directory
const sortedPaths: ModulePath[] = [];
for (
let directory = getDirectoryPath(toPath(importingFileName, cwd, getCanonicalFileName));
let directory = getDirectoryPath(importingFileName);
allFileNames.size !== 0;
) {
const directoryStart = ensureTrailingDirectorySeparator(directory);
@@ -384,6 +382,10 @@ namespace ts.moduleSpecifiers {
if (remainingPaths.length > 1) remainingPaths.sort(comparePathsByRedirectAndNumberOfDirectorySeparators);
sortedPaths.push(...remainingPaths);
}
if (cache) {
cache.set(importingFileName, toPath(importedFileName, host.getCurrentDirectory(), getCanonicalFileName), sortedPaths);
}
return sortedPaths;
}
+18
View File
@@ -8015,6 +8015,7 @@ namespace ts {
readFile?(path: string): string | undefined;
realpath?(path: string): string;
getSymlinkCache?(): SymlinkCache;
getModuleSpecifierCache?(): ModuleSpecifierCache;
getGlobalTypingsCacheLocation?(): string | undefined;
getNearestAncestorDirectoryWithPackageJson?(fileName: string, rootDir?: string): string | undefined;
@@ -8025,6 +8026,21 @@ namespace ts {
getFileIncludeReasons(): MultiMap<Path, FileIncludeReason>;
}
/* @internal */
export interface ModulePath {
path: string;
isInNodeModules: boolean;
isRedirect: boolean;
}
/* @internal */
export interface ModuleSpecifierCache {
get(fromFileName: Path, toFileName: Path): boolean | readonly ModulePath[] | undefined;
set(fromFileName: Path, toFileName: Path, moduleSpecifiers: boolean | readonly ModulePath[]): void;
clear(): void;
count(): number;
}
// Note: this used to be deprecated in our public API, but is still used internally
/* @internal */
export interface SymbolTracker {
@@ -8314,6 +8330,8 @@ namespace ts {
readonly disableSuggestions?: boolean;
readonly quotePreference?: "auto" | "double" | "single";
readonly includeCompletionsForModuleExports?: boolean;
readonly includeCompletionsForImportStatements?: boolean;
readonly includeCompletionsWithSnippetText?: boolean;
readonly includeAutomaticOptionalChainCompletions?: boolean;
readonly includeCompletionsWithInsertText?: boolean;
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
+13
View File
@@ -2412,6 +2412,19 @@ namespace ts {
return decl.kind === SyntaxKind.FunctionDeclaration || isVariableDeclaration(decl) && decl.initializer && isFunctionLike(decl.initializer);
}
export function tryGetModuleSpecifierFromDeclaration(node: AnyImportOrRequire): string | undefined {
switch (node.kind) {
case SyntaxKind.VariableDeclaration:
return node.initializer.arguments[0].text;
case SyntaxKind.ImportDeclaration:
return tryCast(node.moduleSpecifier, isStringLiteralLike)?.text;
case SyntaxKind.ImportEqualsDeclaration:
return tryCast(tryCast(node.moduleReference, isExternalModuleReference)?.expression, isStringLiteralLike)?.text;
default:
Debug.assertNever(node);
}
}
export function importFromModuleSpecifier(node: StringLiteralLike): AnyValidImportOrReExport {
return tryGetImportFromModuleSpecifier(node) || Debug.failBadSyntaxKind(node.parent);
}
+9 -2
View File
@@ -930,8 +930,12 @@ namespace FourSlash {
assert.equal(actual.hasAction, expected.hasAction, `Expected 'hasAction' properties to match`);
assert.equal(actual.isRecommended, expected.isRecommended, `Expected 'isRecommended' properties to match'`);
assert.equal(actual.isSnippet, expected.isSnippet, `Expected 'isSnippet' properties to match`);
assert.equal(actual.source, expected.source, `Expected 'source' values to match`);
assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, this.messageAtLastKnownMarker(`Actual entry: ${JSON.stringify(actual)}`));
assert.equal(actual.sortText, expected.sortText || ts.Completions.SortText.LocationPriority, `Expected 'sortText' properties to match`);
if (expected.sourceDisplay && actual.sourceDisplay) {
assert.equal(ts.displayPartsToString(actual.sourceDisplay), expected.sourceDisplay, `Expected 'sourceDisplay' properties to match`);
}
if (expected.text !== undefined) {
const actualDetails = ts.Debug.checkDefined(this.getCompletionEntryDetails(actual.name, actual.source, actual.data), `No completion details available for name '${actual.name}' and source '${actual.source}'`);
@@ -941,10 +945,13 @@ namespace FourSlash {
// assert.equal(actualDetails.kind, actual.kind);
assert.equal(actualDetails.kindModifiers, actual.kindModifiers, "Expected 'kindModifiers' properties to match");
assert.equal(actualDetails.source && ts.displayPartsToString(actualDetails.source), expected.sourceDisplay, "Expected 'sourceDisplay' property to match 'source' display parts string");
if (!actual.sourceDisplay) {
assert.equal(actualDetails.sourceDisplay && ts.displayPartsToString(actualDetails.sourceDisplay), expected.sourceDisplay, "Expected 'sourceDisplay' property to match 'sourceDisplay' display parts string");
}
assert.deepEqual(actualDetails.tags, expected.tags);
}
else {
assert(expected.documentation === undefined && expected.tags === undefined && expected.sourceDisplay === undefined, "If specifying completion details, should specify 'text'");
assert(expected.documentation === undefined && expected.tags === undefined, "If specifying completion details, should specify 'text'");
}
}
+1
View File
@@ -1599,6 +1599,7 @@ namespace FourSlashInterface {
readonly isFromUncheckedFile?: boolean; // If not specified, won't assert about this
readonly kind?: string; // If not specified, won't assert about this
readonly isPackageJsonImport?: boolean; // If not specified, won't assert about this
readonly isSnippet?: boolean;
readonly kindModifiers?: string; // Must be paired with 'kind'
readonly text?: string;
readonly documentation?: string;
+13 -12
View File
@@ -2958,7 +2958,7 @@ namespace ts.server {
});
}
if (includePackageJsonAutoImports !== args.preferences.includePackageJsonAutoImports) {
this.invalidateProjectAutoImports(/*packageJsonPath*/ undefined);
this.invalidateProjectPackageJson(/*packageJsonPath*/ undefined);
}
}
if (args.extraFileExtensions) {
@@ -4041,7 +4041,7 @@ namespace ts.server {
private watchPackageJsonFile(path: Path) {
const watchers = this.packageJsonFilesMap || (this.packageJsonFilesMap = new Map());
if (!watchers.has(path)) {
this.invalidateProjectAutoImports(path);
this.invalidateProjectPackageJson(path);
watchers.set(path, this.watchFactory.watchFile(
path,
(fileName, eventKind) => {
@@ -4051,11 +4051,11 @@ namespace ts.server {
return Debug.fail();
case FileWatcherEventKind.Changed:
this.packageJsonCache.addOrUpdate(path);
this.invalidateProjectAutoImports(path);
this.invalidateProjectPackageJson(path);
break;
case FileWatcherEventKind.Deleted:
this.packageJsonCache.delete(path);
this.invalidateProjectAutoImports(path);
this.invalidateProjectPackageJson(path);
watchers.get(path)!.close();
watchers.delete(path);
}
@@ -4083,15 +4083,16 @@ namespace ts.server {
}
/*@internal*/
private invalidateProjectAutoImports(packageJsonPath: Path | undefined) {
if (this.includePackageJsonAutoImports()) {
this.configuredProjects.forEach(invalidate);
this.inferredProjects.forEach(invalidate);
this.externalProjects.forEach(invalidate);
}
private invalidateProjectPackageJson(packageJsonPath: Path | undefined) {
this.configuredProjects.forEach(invalidate);
this.inferredProjects.forEach(invalidate);
this.externalProjects.forEach(invalidate);
function invalidate(project: Project) {
if (!packageJsonPath || project.packageJsonsForAutoImport?.has(packageJsonPath)) {
project.markAutoImportProviderAsDirty();
if (packageJsonPath) {
project.onPackageJsonChange(packageJsonPath);
}
else {
project.onAutoImportProviderSettingsChanged();
}
}
}
+62 -73
View File
@@ -146,6 +146,8 @@ namespace ts.server {
lastCachedUnresolvedImportsList: SortedReadonlyArray<string> | undefined;
/*@internal*/
private hasAddedorRemovedFiles = false;
/*@internal*/
private hasAddedOrRemovedSymlinks = false;
/*@internal*/
lastFileExceededProgramSize: string | undefined;
@@ -204,7 +206,7 @@ namespace ts.server {
originalConfiguredProjects: Set<NormalizedPath> | undefined;
/*@internal*/
packageJsonsForAutoImport: Set<string> | undefined;
private packageJsonsForAutoImport: Set<string> | undefined;
/*@internal*/
getResolvedProjectReferenceToRedirect(_fileName: string): ResolvedProjectReference | undefined {
@@ -248,9 +250,11 @@ namespace ts.server {
public readonly getCanonicalFileName: GetCanonicalFileName;
/*@internal*/
private importSuggestionsCache = Completions.createImportSuggestionsForFileCache();
private exportMapCache = createExportMapCache();
/*@internal*/
private dirtyFilesForSuggestions: Set<Path> | undefined;
private changedFilesForExportMapCache: Set<Path> | undefined;
/*@internal*/
private moduleSpecifierCache = createModuleSpecifierCache();
/*@internal*/
private symlinks: SymlinkCache | undefined;
/*@internal*/
@@ -979,8 +983,8 @@ namespace ts.server {
/*@internal*/
markFileAsDirty(changedFile: Path) {
this.markAsDirty();
if (!this.importSuggestionsCache.isEmpty()) {
(this.dirtyFilesForSuggestions || (this.dirtyFilesForSuggestions = new Set())).add(changedFile);
if (!this.exportMapCache.isEmpty()) {
(this.changedFilesForExportMapCache ||= new Set()).add(changedFile);
}
}
@@ -992,17 +996,31 @@ namespace ts.server {
}
/*@internal*/
markAutoImportProviderAsDirty() {
onAutoImportProviderSettingsChanged() {
if (this.autoImportProviderHost === false) {
this.autoImportProviderHost = undefined;
}
this.autoImportProviderHost?.markAsDirty();
this.importSuggestionsCache.clear();
else {
this.autoImportProviderHost?.markAsDirty();
}
}
/*@internal*/
onPackageJsonChange(packageJsonPath: Path) {
if (this.packageJsonsForAutoImport?.has(packageJsonPath)) {
this.moduleSpecifierCache.clear();
if (this.autoImportProviderHost) {
this.autoImportProviderHost.markAsDirty();
}
}
}
/* @internal */
onFileAddedOrRemoved() {
onFileAddedOrRemoved(isSymlink: boolean | undefined) {
this.hasAddedorRemovedFiles = true;
if (isSymlink) {
this.hasAddedOrRemovedSymlinks = true;
}
}
/**
@@ -1016,6 +1034,7 @@ namespace ts.server {
const hasNewProgram = this.updateGraphWorker();
const hasAddedorRemovedFiles = this.hasAddedorRemovedFiles;
this.hasAddedorRemovedFiles = false;
this.hasAddedOrRemovedSymlinks = false;
const changedFiles: readonly Path[] = this.resolutionCache.finishRecordingFilesWithChangedResolutions() || emptyArray;
@@ -1166,27 +1185,30 @@ namespace ts.server {
}
}
if (!this.importSuggestionsCache.isEmpty()) {
if (!this.exportMapCache.isEmpty()) {
if (this.hasAddedorRemovedFiles || oldProgram && !this.program!.structureIsReused) {
this.importSuggestionsCache.clear();
this.exportMapCache.clear();
}
else if (this.dirtyFilesForSuggestions && oldProgram && this.program) {
forEachKey(this.dirtyFilesForSuggestions, fileName => {
const oldSourceFile = oldProgram.getSourceFile(fileName);
const sourceFile = this.program!.getSourceFile(fileName);
if (this.sourceFileHasChangedOwnImportSuggestions(oldSourceFile, sourceFile)) {
this.importSuggestionsCache.clear();
else if (this.changedFilesForExportMapCache && oldProgram && this.program) {
forEachKey(this.changedFilesForExportMapCache, fileName => {
const oldSourceFile = oldProgram.getSourceFileByPath(fileName);
const sourceFile = this.program!.getSourceFileByPath(fileName);
if (!oldSourceFile || !sourceFile) {
this.exportMapCache.clear();
return true;
}
return this.exportMapCache.onFileChanged(oldSourceFile, sourceFile, !!this.getTypeAcquisition().enable);
});
}
}
if (this.dirtyFilesForSuggestions) {
this.dirtyFilesForSuggestions.clear();
if (this.changedFilesForExportMapCache) {
this.changedFilesForExportMapCache.clear();
}
if (this.hasAddedorRemovedFiles) {
if (this.hasAddedOrRemovedSymlinks || this.program && !this.program.structureIsReused && this.getCompilerOptions().preserveSymlinks) {
// With --preserveSymlinks, we may not determine that a file is a symlink, so we never set `hasAddedOrRemovedSymlinks`
this.symlinks = undefined;
this.moduleSpecifierCache.clear();
}
const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray<string>;
@@ -1218,54 +1240,6 @@ namespace ts.server {
this.projectService.sendPerformanceEvent(kind, durationMs);
}
/*@internal*/
private sourceFileHasChangedOwnImportSuggestions(oldSourceFile: SourceFile | undefined, newSourceFile: SourceFile | undefined) {
if (!oldSourceFile && !newSourceFile) {
return false;
}
// Probably shouldn’t get this far, but on the off chance the file was added or removed,
// we can’t reliably tell anything about it.
if (!oldSourceFile || !newSourceFile) {
return true;
}
Debug.assertEqual(oldSourceFile.fileName, newSourceFile.fileName);
// If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node.
// Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list.
if (this.getTypeAcquisition().enable && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile)) {
return true;
}
// Module agumentation and ambient module changes can add or remove exports available to be auto-imported.
// Changes elsewhere in the file can change the *type* of an export in a module augmentation,
// but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache.
if (
!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) ||
!this.ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile)
) {
return true;
}
return false;
}
/*@internal*/
private ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) {
if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) {
return false;
}
let oldFileStatementIndex = -1;
let newFileStatementIndex = -1;
for (const ambientModuleName of newSourceFile.ambientModuleNames) {
const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName;
oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1);
newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1);
if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) {
return false;
}
}
return true;
}
private detachScriptInfoFromProject(uncheckedFileName: string, noRemoveResolution?: boolean) {
const scriptInfoToDetach = this.projectService.getScriptInfo(uncheckedFileName);
if (scriptInfoToDetach) {
@@ -1683,8 +1657,13 @@ namespace ts.server {
}
/*@internal*/
getImportSuggestionsCache() {
return this.importSuggestionsCache;
getExportMapCache() {
return this.exportMapCache;
}
/*@internal*/
getModuleSpecifierCache() {
return this.moduleSpecifierCache;
}
/*@internal*/
@@ -2016,8 +1995,12 @@ namespace ts.server {
this.projectService.setFileNamesOfAutoImportProviderProject(this, rootFileNames);
this.rootFileNames = rootFileNames;
this.hostProject.getImportSuggestionsCache().clear();
return super.updateGraph();
const oldProgram = this.getCurrentProgram();
const hasSameSetOfFiles = super.updateGraph();
if (oldProgram && oldProgram !== this.getCurrentProgram()) {
this.hostProject.getExportMapCache().clear();
}
return hasSameSetOfFiles;
}
hasRoots() {
@@ -2037,10 +2020,16 @@ namespace ts.server {
throw new Error("AutoImportProviderProject language service should never be used. To get the program, use `project.getCurrentProgram()`.");
}
markAutoImportProviderAsDirty(): never {
/*@internal*/
onAutoImportProviderSettingsChanged(): never {
throw new Error("AutoImportProviderProject is an auto import provider; use `markAsDirty()` instead.");
}
/*@internal*/
onPackageJsonChange(): never {
throw new Error("package.json changes should be notified on an AutoImportProvider's host project");
}
getModuleResolutionHostForAutoImportProvider(): never {
throw new Error("AutoImportProviderProject cannot provide its own host; use `hostProject.getModuleResolutionHostForAutomImportProvider()` instead.");
}
+25 -2
View File
@@ -2142,7 +2142,7 @@ namespace ts.server.protocol {
arguments: FormatOnKeyRequestArgs;
}
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#";
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
/**
* Arguments for completions messages.
@@ -2253,6 +2253,10 @@ namespace ts.server.protocol {
* coupled with `replacementSpan` to replace a dotted access with a bracket access.
*/
insertText?: string;
/**
* `insertText` should be interpreted as a snippet if true.
*/
isSnippet?: true;
/**
* An optional span that indicates the text to be replaced by this completion item.
* If present, this span should be used instead of the default one.
@@ -2268,6 +2272,10 @@ namespace ts.server.protocol {
* Identifier (not necessarily human-readable) identifying where this completion came from.
*/
source?: string;
/**
* Human-readable description of the `source`.
*/
sourceDisplay?: SymbolDisplayPart[];
/**
* If true, this completion should be highlighted as recommended. There will only be one of these.
* This will be set when we know the user should write an expression with a certain type and that type is an enum or constructable class.
@@ -2329,9 +2337,14 @@ namespace ts.server.protocol {
codeActions?: CodeAction[];
/**
* Human-readable description of the `source` from the CompletionEntry.
* @deprecated Use `sourceDisplay` instead.
*/
source?: SymbolDisplayPart[];
/**
* Human-readable description of the `source` from the CompletionEntry.
*/
sourceDisplay?: SymbolDisplayPart[];
}
/** @deprecated Prefer CompletionInfoResponse, which supports several top-level fields in addition to the array of entries. */
@@ -2353,6 +2366,7 @@ namespace ts.server.protocol {
* must be used to commit that completion entry.
*/
readonly optionalReplacementSpan?: TextSpan;
readonly isIncomplete?: boolean;
readonly entries: readonly CompletionEntry[];
}
@@ -3298,6 +3312,15 @@ namespace ts.server.protocol {
* This affects lone identifier completions but not completions on the right hand side of `obj.`.
*/
readonly includeCompletionsForModuleExports?: boolean;
/**
* Enables auto-import-style completions on partially-typed import statements. E.g., allows
* `import write|` to be completed to `import { writeFile } from "fs"`.
*/
readonly includeCompletionsForImportStatements?: boolean;
/**
* Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`.
*/
readonly includeCompletionsWithSnippetText?: boolean;
/**
* If enabled, the completion list will include completions with invalid identifier names.
* For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`.
+15 -6
View File
@@ -415,6 +415,15 @@ namespace ts.server {
return this.realpath && this.realpath !== this.path ? this.realpath : undefined;
}
/**
* @internal
* Does not compute realpath; uses precomputed result. Use `ensureRealPath`
* first if a definite result is needed.
*/
isSymlink(): boolean | undefined {
return this.realpath && this.realpath !== this.path;
}
getFormatCodeSettings(): FormatCodeSettings | undefined { return this.formatSettings; }
getPreferences(): protocol.UserPreferences | undefined { return this.preferences; }
@@ -422,10 +431,10 @@ namespace ts.server {
const isNew = !this.isAttached(project);
if (isNew) {
this.containingProjects.push(project);
project.onFileAddedOrRemoved();
if (!project.getCompilerOptions().preserveSymlinks) {
this.ensureRealPath();
}
project.onFileAddedOrRemoved(this.isSymlink());
}
return isNew;
}
@@ -447,23 +456,23 @@ namespace ts.server {
return;
case 1:
if (this.containingProjects[0] === project) {
project.onFileAddedOrRemoved();
project.onFileAddedOrRemoved(this.isSymlink());
this.containingProjects.pop();
}
break;
case 2:
if (this.containingProjects[0] === project) {
project.onFileAddedOrRemoved();
project.onFileAddedOrRemoved(this.isSymlink());
this.containingProjects[0] = this.containingProjects.pop()!;
}
else if (this.containingProjects[1] === project) {
project.onFileAddedOrRemoved();
project.onFileAddedOrRemoved(this.isSymlink());
this.containingProjects.pop();
}
break;
default:
if (unorderedRemoveItem(this.containingProjects, project)) {
project.onFileAddedOrRemoved();
project.onFileAddedOrRemoved(this.isSymlink());
}
break;
}
@@ -477,7 +486,7 @@ namespace ts.server {
const existingRoot = p.getRootFilesMap().get(this.path);
// detach is unnecessary since we'll clean the list of containing projects anyways
p.removeFile(this, /*fileExists*/ false, /*detachFromProjects*/ false);
p.onFileAddedOrRemoved();
p.onFileAddedOrRemoved(this.isSymlink());
// If the info was for the external or configured project's root,
// add missing file as the root
if (existingRoot && !isInferredProject(p)) {
+2 -2
View File
@@ -1838,10 +1838,10 @@ namespace ts.server {
const prefix = args.prefix || "";
const entries = stableSort(mapDefined<CompletionEntry, protocol.CompletionEntry>(completions.entries, entry => {
if (completions.isMemberCompletion || startsWith(entry.name.toLowerCase(), prefix.toLowerCase())) {
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, isRecommended, isPackageJsonImport, data } = entry;
const { name, kind, kindModifiers, sortText, insertText, replacementSpan, hasAction, source, sourceDisplay, isSnippet, isRecommended, isPackageJsonImport, data } = entry;
const convertedSpan = replacementSpan ? toProtocolTextSpan(replacementSpan, scriptInfo) : undefined;
// Use `hasAction || undefined` to avoid serializing `false`.
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, hasAction: hasAction || undefined, source, isRecommended, isPackageJsonImport, data };
return { name, kind, kindModifiers, sortText, insertText, replacementSpan: convertedSpan, isSnippet, hasAction: hasAction || undefined, source, sourceDisplay, isRecommended, isPackageJsonImport, data };
}
}), (a, b) => compareStringsCaseSensitiveUI(a.name, b.name));
+159 -268
View File
@@ -156,11 +156,13 @@ namespace ts.codefix {
readonly kind: ImportFixKind.UseNamespace;
readonly namespacePrefix: string;
readonly position: number;
readonly moduleSpecifier: string;
}
interface FixUseImportType {
readonly kind: ImportFixKind.ImportType;
readonly moduleSpecifier: string;
readonly position: number;
readonly exportInfo: SymbolExportInfo;
}
interface FixAddToExistingImport {
readonly kind: ImportFixKind.AddToExisting;
@@ -175,21 +177,7 @@ namespace ts.codefix {
readonly importKind: ImportKind;
readonly typeOnly: boolean;
readonly useRequire: boolean;
}
const enum ImportKind {
Named,
Default,
Namespace,
CommonJS,
}
/** Information about how a symbol is exported from a module. (We don't need to store the exported symbol, just its module.) */
interface SymbolExportInfo {
readonly moduleSymbol: Symbol;
readonly importKind: ImportKind;
/** If true, can't use an es6 import from a js file. */
readonly exportedSymbolIsTypeOnly: boolean;
readonly exportInfo?: SymbolExportInfo;
}
/** Information needed to augment an existing import declaration. */
@@ -211,42 +199,40 @@ namespace ts.codefix {
): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } {
const compilerOptions = program.getCompilerOptions();
const exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name))
? [getSymbolExportInfoForSymbol(exportedSymbol, moduleSymbol, sourceFile, program, host)]
? [getSymbolExportInfoForSymbol(exportedSymbol, moduleSymbol, program, host)]
: getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, /*useAutoImportProvider*/ true);
const useRequire = shouldUseRequire(sourceFile, program);
const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && !isSourceFileJS(sourceFile) && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
const moduleSpecifier = getBestFix(getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences), sourceFile, program, host).moduleSpecifier;
const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, useRequire, host, preferences);
return { moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) };
return { moduleSpecifier: fix.moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) };
}
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol");
// We sort the best codefixes first, so taking `first` is best.
return getBestFix(getFixForImport(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, program, host);
return getBestFix(getImportFixes(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, host);
}
function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction {
return { description, changes, commands };
}
function getSymbolExportInfoForSymbol(symbol: Symbol, moduleSymbol: Symbol, importingFile: SourceFile, program: Program, host: LanguageServiceHost): SymbolExportInfo {
function getSymbolExportInfoForSymbol(symbol: Symbol, moduleSymbol: Symbol, program: Program, host: LanguageServiceHost): SymbolExportInfo {
const compilerOptions = program.getCompilerOptions();
const mainProgramInfo = getInfoWithChecker(program.getTypeChecker());
const mainProgramInfo = getInfoWithChecker(program.getTypeChecker(), /*isFromPackageJson*/ false);
if (mainProgramInfo) {
return mainProgramInfo;
}
const autoImportProvider = host.getPackageJsonAutoImportProvider?.()?.getTypeChecker();
return Debug.checkDefined(autoImportProvider && getInfoWithChecker(autoImportProvider), `Could not find symbol in specified module for code actions`);
return Debug.checkDefined(autoImportProvider && getInfoWithChecker(autoImportProvider, /*isFromPackageJson*/ true), `Could not find symbol in specified module for code actions`);
function getInfoWithChecker(checker: TypeChecker): SymbolExportInfo | undefined {
const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions);
function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined {
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) {
return { moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker) };
return { symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson };
}
const named = checker.tryGetMemberInModuleExportsAndProperties(symbol.name, moduleSymbol);
if (named && skipAlias(named, checker) === symbol) {
return { moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker) };
return { symbol: named, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson };
}
}
}
@@ -254,25 +240,92 @@ namespace ts.codefix {
function getAllReExportingModules(importingFile: SourceFile, exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
const result: SymbolExportInfo[] = [];
const compilerOptions = program.getCompilerOptions();
forEachExternalModuleToImportFrom(program, host, importingFile, /*filterByPackageJson*/ false, useAutoImportProvider, (moduleSymbol, moduleFile, program) => {
const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => {
return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host);
});
forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
const checker = program.getTypeChecker();
// Don't import from a re-export when looking "up" like to `./index` or `../index`.
if (moduleFile && moduleSymbol !== exportingModuleSymbol && startsWith(importingFile.fileName, getDirectoryPath(moduleFile.fileName))) {
return;
}
const defaultInfo = getDefaultLikeExportInfo(importingFile, moduleSymbol, checker, compilerOptions);
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol) {
result.push({ moduleSymbol, importKind: defaultInfo.kind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker) });
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
result.push({ symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson });
}
for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol) {
result.push({ moduleSymbol, importKind: ImportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker) });
if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
result.push({ symbol: exported, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson });
}
}
});
return result;
function isImportable(program: Program, moduleFile: SourceFile | undefined, isFromPackageJson: boolean) {
return !moduleFile || isImportableFile(program, importingFile, moduleFile, /*packageJsonFilter*/ undefined, getModuleSpecifierResolutionHost(isFromPackageJson), host.getModuleSpecifierCache?.());
}
}
export function getModuleSpecifierForBestExportInfo(exportInfo: readonly SymbolExportInfo[],
importingFile: SourceFile,
program: Program,
host: LanguageServiceHost,
preferences: UserPreferences
): { exportInfo?: SymbolExportInfo, moduleSpecifier: string } {
return getBestFix(getNewImportFixes(program, importingFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, exportInfo, host, preferences), importingFile, host);
}
export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program) {
const start = timestamp();
// Pulling the AutoImportProvider project will trigger its updateGraph if pending,
// which will invalidate the export map cache if things change, so pull it before
// checking the cache.
host.getPackageJsonAutoImportProvider?.();
const cache = host.getExportMapCache?.();
if (cache) {
const cached = cache.get(importingFile.path, program.getTypeChecker(), host.getProjectVersion?.());
if (cached) {
host.log?.("getSymbolToExportInfoMap: cache hit");
return cached;
}
else {
host.log?.("getSymbolToExportInfoMap: cache miss or empty; calculating new results");
}
}
const result: MultiMap<string, SymbolExportInfo> = createMultiMap();
const compilerOptions = program.getCompilerOptions();
const target = getEmitScriptTarget(compilerOptions);
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, _moduleFile, program, isFromPackageJson) => {
const checker = program.getTypeChecker();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo) {
const name = getNameForExportedSymbol(getLocalSymbolForExportDefault(defaultInfo.symbol) || defaultInfo.symbol, target);
result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), { symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson });
}
const seenExports = new Map<Symbol, true>();
for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
if (exported !== defaultInfo?.symbol && addToSeen(seenExports, exported)) {
result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), { symbol: exported, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson });
}
}
});
if (cache) {
host.log?.("getSymbolToExportInfoMap: caching results");
cache.set(result, host.getProjectVersion?.());
}
host.log?.(`getSymbolToExportInfoMap: done in ${timestamp() - start} ms`);
return result;
function key(name: string, alias: Symbol, moduleSymbol: Symbol, checker: TypeChecker) {
const moduleName = stripQuotes(moduleSymbol.name);
const moduleKey = isExternalModuleNameRelative(moduleName) ? "/" : moduleName;
return `${name}|${getSymbolId(skipAlias(alias, checker))}|${moduleKey}`;
}
}
function isTypeOnlySymbol(s: Symbol, checker: TypeChecker): boolean {
@@ -283,7 +336,7 @@ namespace ts.codefix {
return isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
}
function getFixForImport(
function getImportFixes(
exportInfos: readonly SymbolExportInfo[],
symbolName: string,
/** undefined only for missing JSX namespace */
@@ -296,7 +349,7 @@ namespace ts.codefix {
preferences: UserPreferences,
): readonly ImportFix[] {
const checker = program.getTypeChecker();
const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, checker, sourceFile));
const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, checker, sourceFile, program.getCompilerOptions()));
const useNamespace = position === undefined ? undefined : tryUseExistingNamespaceImport(existingImports, symbolName, position, checker);
const addToExisting = tryAddToExistingImport(existingImports, position !== undefined && isTypeOnlyPosition(sourceFile, position));
// Don't bother providing an action to add a new import if we can add to an existing one.
@@ -319,10 +372,11 @@ namespace ts.codefix {
// and it is up to the user to decide which one fits best.
return firstDefined(existingImports, ({ declaration }): FixUseNamespaceImport | undefined => {
const namespacePrefix = getNamespaceLikeImportText(declaration);
if (namespacePrefix) {
const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration);
if (namespacePrefix && moduleSpecifier) {
const moduleSymbol = getTargetModuleFromNamespaceLikeImport(declaration, checker);
if (moduleSymbol && moduleSymbol.exports!.has(escapeLeadingUnderscores(symbolName))) {
return { kind: ImportFixKind.UseNamespace, namespacePrefix, position };
return { kind: ImportFixKind.UseNamespace, namespacePrefix, position, moduleSpecifier };
}
}
});
@@ -364,17 +418,19 @@ namespace ts.codefix {
: undefined;
}
const { importClause } = declaration;
if (!importClause) return undefined;
if (!importClause || !isStringLiteralLike(declaration.moduleSpecifier)) return undefined;
const { name, namedBindings } = importClause;
return importKind === ImportKind.Default && !name || importKind === ImportKind.Named && (!namedBindings || namedBindings.kind === SyntaxKind.NamedImports)
? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: importClause, importKind, moduleSpecifier: declaration.moduleSpecifier.getText(), canUseTypeOnlyImport }
? { kind: ImportFixKind.AddToExisting, importClauseOrBindingPattern: importClause, importKind, moduleSpecifier: declaration.moduleSpecifier.text, canUseTypeOnlyImport }
: undefined;
});
}
function getExistingImportDeclarations({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }: SymbolExportInfo, checker: TypeChecker, sourceFile: SourceFile): readonly FixAddToExistingImportInfo[] {
function getExistingImportDeclarations({ moduleSymbol, exportKind, exportedSymbolIsTypeOnly }: SymbolExportInfo, checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions): readonly FixAddToExistingImportInfo[] {
// Can't use an es6 import for a type in JS.
return exportedSymbolIsTypeOnly && isSourceFileJS(sourceFile) ? emptyArray : mapDefined(sourceFile.imports, (moduleSpecifier): FixAddToExistingImportInfo | undefined => {
if (exportedSymbolIsTypeOnly && isSourceFileJS(importingFile)) return emptyArray;
const importKind = getImportKind(importingFile, exportKind, compilerOptions);
return mapDefined(importingFile.imports, (moduleSpecifier): FixAddToExistingImportInfo | undefined => {
const i = importFromModuleSpecifier(moduleSpecifier);
if (isRequireVariableDeclaration(i.parent)) {
return checker.resolveExternalModuleName(moduleSpecifier) === moduleSymbol ? { declaration: i.parent, importKind } : undefined;
@@ -412,7 +468,7 @@ namespace ts.codefix {
return true;
}
function getNewImportInfos(
function getNewImportFixes(
program: Program,
sourceFile: SourceFile,
position: number | undefined,
@@ -424,13 +480,14 @@ namespace ts.codefix {
): readonly (FixAddNewImport | FixUseImportType)[] {
const isJs = isSourceFileJS(sourceFile);
const compilerOptions = program.getCompilerOptions();
return flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) =>
moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getTypeChecker(), compilerOptions, sourceFile, createModuleSpecifierResolutionHost(program, host), preferences)
const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host);
return flatMap(moduleSymbols, exportInfo =>
moduleSpecifiers.getModuleSpecifiers(exportInfo.moduleSymbol, program.getTypeChecker(), compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences)
.map((moduleSpecifier): FixAddNewImport | FixUseImportType =>
// `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types.
exportedSymbolIsTypeOnly && isJs
? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.checkDefined(position, "position should be defined") }
: { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, useRequire, typeOnly: preferTypeOnlyImport }));
exportInfo.exportedSymbolIsTypeOnly && isJs && position !== undefined
? { kind: ImportFixKind.ImportType, moduleSpecifier, position, exportInfo }
: { kind: ImportFixKind.AddNew, moduleSpecifier, importKind: getImportKind(sourceFile, exportInfo.exportKind, compilerOptions), useRequire, typeOnly: preferTypeOnlyImport, exportInfo }));
}
function getFixesForAddImport(
@@ -445,16 +502,13 @@ namespace ts.codefix {
preferences: UserPreferences,
): readonly (FixAddNewImport | FixUseImportType)[] {
const existingDeclaration = firstDefined(existingImports, info => newImportInfoFromExistingSpecifier(info, preferTypeOnlyImport, useRequire));
return existingDeclaration ? [existingDeclaration] : getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences);
return existingDeclaration ? [existingDeclaration] : getNewImportFixes(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences);
}
function newImportInfoFromExistingSpecifier({ declaration, importKind }: FixAddToExistingImportInfo, preferTypeOnlyImport: boolean, useRequire: boolean): FixAddNewImport | undefined {
const moduleSpecifier = declaration.kind === SyntaxKind.ImportDeclaration ? declaration.moduleSpecifier :
declaration.kind === SyntaxKind.VariableDeclaration ? declaration.initializer.arguments[0] :
declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference ? declaration.moduleReference.expression :
undefined;
return moduleSpecifier && isStringLiteral(moduleSpecifier)
? { kind: ImportFixKind.AddNew, moduleSpecifier: moduleSpecifier.text, importKind, typeOnly: preferTypeOnlyImport, useRequire }
const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(declaration);
return moduleSpecifier
? { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, typeOnly: preferTypeOnlyImport, useRequire }
: undefined;
}
@@ -464,20 +518,20 @@ namespace ts.codefix {
const info = errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code
? getFixesInfoForUMDImport(context, symbolToken)
: isIdentifier(symbolToken) ? getFixesInfoForNonUMDImport(context, symbolToken, useAutoImportProvider) : undefined;
return info && { ...info, fixes: sortFixes(info.fixes, context.sourceFile, context.program, context.host) };
return info && { ...info, fixes: sortFixes(info.fixes, context.sourceFile, context.host) };
}
function sortFixes(fixes: readonly ImportFix[], sourceFile: SourceFile, program: Program, host: LanguageServiceHost): readonly ImportFix[] {
const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host);
function sortFixes(fixes: readonly ImportFix[], sourceFile: SourceFile, host: LanguageServiceHost): readonly ImportFix[] {
const { allowsImportingSpecifier } = createPackageJsonImportFilter(sourceFile, host);
return sort(fixes, (a, b) => compareValues(a.kind, b.kind) || compareModuleSpecifiers(a, b, allowsImportingSpecifier));
}
function getBestFix<T extends ImportFix>(fixes: readonly T[], sourceFile: SourceFile, program: Program, host: LanguageServiceHost): T {
function getBestFix<T extends ImportFix>(fixes: readonly T[], sourceFile: SourceFile, host: LanguageServiceHost): T {
// These will always be placed first if available, and are better than other kinds
if (fixes[0].kind === ImportFixKind.UseNamespace || fixes[0].kind === ImportFixKind.AddToExisting) {
return fixes[0];
}
const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host);
const { allowsImportingSpecifier } = createPackageJsonImportFilter(sourceFile, host);
return fixes.reduce((best, fix) =>
compareModuleSpecifiers(fix, best, allowsImportingSpecifier) === Comparison.LessThan ? fix : best
);
@@ -497,9 +551,9 @@ namespace ts.codefix {
if (!umdSymbol) return undefined;
const symbol = checker.getAliasedSymbol(umdSymbol);
const symbolName = umdSymbol.name;
const exportInfos: readonly SymbolExportInfo[] = [{ moduleSymbol: symbol, importKind: getUmdImportKind(sourceFile, program.getCompilerOptions()), exportedSymbolIsTypeOnly: false }];
const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, exportKind: ExportKind.UMD, exportedSymbolIsTypeOnly: false, isFromPackageJson: false }];
const useRequire = shouldUseRequire(sourceFile, program);
const fixes = getFixForImport(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences);
const fixes = getImportFixes(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences);
return { fixes, symbolName };
}
function getUmdSymbol(token: Node, checker: TypeChecker): Symbol | undefined {
@@ -514,6 +568,16 @@ namespace ts.codefix {
: undefined;
}
export function getImportKind(importingFile: SourceFile, exportKind: ExportKind, compilerOptions: CompilerOptions): ImportKind {
switch (exportKind) {
case ExportKind.Named: return ImportKind.Named;
case ExportKind.Default: return ImportKind.Default;
case ExportKind.ExportEquals: return getExportEqualsImportKind(importingFile, compilerOptions);
case ExportKind.UMD: return getUmdImportKind(importingFile, compilerOptions);
default: return Debug.assertNever(exportKind);
}
}
function getUmdImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions): ImportKind {
// Import a synthetic `default` if enabled.
if (getAllowSyntheticDefaultImports(compilerOptions)) {
@@ -553,7 +617,7 @@ namespace ts.codefix {
const useRequire = shouldUseRequire(sourceFile, program);
const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host);
const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) =>
getFixForImport(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences)));
getImportFixes(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences)));
return { fixes, symbolName };
}
@@ -573,7 +637,7 @@ namespace ts.codefix {
symbolName: string,
currentTokenMeaning: SemanticMeaning,
cancellationToken: CancellationToken,
sourceFile: SourceFile,
fromFile: SourceFile,
program: Program,
useAutoImportProvider: boolean,
host: LanguageServiceHost
@@ -581,43 +645,52 @@ namespace ts.codefix {
// For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once.
// Maps symbol id to info for modules providing that symbol (original export + re-exports).
const originalSymbolToExportInfos = createMultiMap<SymbolExportInfo>();
function addSymbol(moduleSymbol: Symbol, exportedSymbol: Symbol, importKind: ImportKind, checker: TypeChecker): void {
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { moduleSymbol, importKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker) });
const packageJsonFilter = createPackageJsonImportFilter(fromFile, host);
const moduleSpecifierCache = host.getModuleSpecifierCache?.();
const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => {
return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host);
});
function addSymbol(moduleSymbol: Symbol, toFile: SourceFile | undefined, exportedSymbol: Symbol, exportKind: ExportKind, program: Program, isFromPackageJson: boolean): void {
const moduleSpecifierResolutionHost = getModuleSpecifierResolutionHost(isFromPackageJson);
if (toFile && isImportableFile(program, fromFile, toFile, packageJsonFilter, moduleSpecifierResolutionHost, moduleSpecifierCache) ||
!toFile && packageJsonFilter.allowsImportingAmbientModule(moduleSymbol, moduleSpecifierResolutionHost)
) {
const checker = program.getTypeChecker();
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson });
}
}
forEachExternalModuleToImportFrom(program, host, sourceFile, /*filterByPackageJson*/ true, useAutoImportProvider, (moduleSymbol, _, program) => {
forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => {
const checker = program.getTypeChecker();
cancellationToken.throwIfCancellationRequested();
const compilerOptions = program.getCompilerOptions();
const defaultInfo = getDefaultLikeExportInfo(sourceFile, moduleSymbol, checker, compilerOptions);
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) {
addSymbol(moduleSymbol, defaultInfo.symbol, defaultInfo.kind, checker);
addSymbol(moduleSymbol, sourceFile, defaultInfo.symbol, defaultInfo.exportKind, program, isFromPackageJson);
}
// check exports with the same name
const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol);
if (exportSymbolWithIdenticalName && symbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) {
addSymbol(moduleSymbol, exportSymbolWithIdenticalName, ImportKind.Named, checker);
addSymbol(moduleSymbol, sourceFile, exportSymbolWithIdenticalName, ExportKind.Named, program, isFromPackageJson);
}
});
return originalSymbolToExportInfos;
}
function getDefaultLikeExportInfo(
importingFile: SourceFile, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions,
): { readonly symbol: Symbol, readonly symbolForMeaning: Symbol, readonly name: string, readonly kind: ImportKind } | undefined {
const exported = getDefaultLikeExportWorker(importingFile, moduleSymbol, checker, compilerOptions);
function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions) {
const exported = getDefaultLikeExportWorker(moduleSymbol, checker);
if (!exported) return undefined;
const { symbol, kind } = exported;
const { symbol, exportKind } = exported;
const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions);
return info && { symbol, kind, ...info };
return info && { symbol, exportKind, ...info };
}
function getDefaultLikeExportWorker(importingFile: SourceFile, moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbol: Symbol, readonly kind: ImportKind } | undefined {
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
if (defaultExport) return { symbol: defaultExport, kind: ImportKind.Default };
function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly exportKind: ExportKind } | undefined {
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
return exportEquals === moduleSymbol ? undefined : { symbol: exportEquals, kind: getExportEqualsImportKind(importingFile, compilerOptions) };
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default };
}
function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions): ImportKind {
@@ -636,7 +709,8 @@ namespace ts.codefix {
// really hate that, so look to see if the importing file has any precedent
// on how to handle it.
for (const statement of importingFile.statements) {
if (isImportEqualsDeclaration(statement)) {
// `import foo` parses as an ImportEqualsDeclaration even though it could be an ImportDeclaration
if (isImportEqualsDeclaration(statement) && !nodeIsMissing(statement.moduleReference)) {
return ImportKind.CommonJS;
}
}
@@ -880,57 +954,23 @@ namespace ts.codefix {
export function forEachExternalModuleToImportFrom(
program: Program,
host: LanguageServiceHost,
from: SourceFile,
filterByPackageJson: boolean,
useAutoImportProvider: boolean,
cb: (module: Symbol, moduleFile: SourceFile | undefined, program: Program, isFromPackageJson: boolean) => void,
) {
forEachExternalModuleToImportFromInProgram(program, host, from, filterByPackageJson, (module, file) => cb(module, file, program, /*isFromPackageJson*/ false));
forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => cb(module, file, program, /*isFromPackageJson*/ false));
const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.();
if (autoImportProvider) {
const start = timestamp();
forEachExternalModuleToImportFromInProgram(autoImportProvider, host, from, filterByPackageJson, (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true));
forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true));
host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`);
}
}
function forEachExternalModuleToImportFromInProgram(
program: Program,
host: LanguageServiceHost,
from: SourceFile,
filterByPackageJson: boolean,
cb: (module: Symbol, moduleFile: SourceFile | undefined) => void,
) {
let filteredCount = 0;
const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host);
const packageJson = filterByPackageJson && createAutoImportFilter(from, program, host, moduleSpecifierResolutionHost);
forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, sourceFile) => {
if (sourceFile === undefined) {
if (!packageJson || packageJson.allowsImportingAmbientModule(module)) {
cb(module, sourceFile);
}
else if (packageJson) {
filteredCount++;
}
}
else if (sourceFile &&
sourceFile !== from &&
isImportableFile(program, from, sourceFile, moduleSpecifierResolutionHost)
) {
if (!packageJson || packageJson.allowsImportingSourceFile(sourceFile)) {
cb(module, sourceFile);
}
else if (packageJson) {
filteredCount++;
}
}
});
host.log?.(`forEachExternalModuleToImportFrom: filtered out ${filteredCount} modules by package.json contents`);
}
function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
for (const ambient of checker.getAmbientModules()) {
cb(ambient, /*sourceFile*/ undefined);
if (!stringContains(ambient.name, "*")) {
cb(ambient, /*sourceFile*/ undefined);
}
}
for (const sourceFile of allSourceFiles) {
if (isExternalOrCommonJsModule(sourceFile)) {
@@ -939,42 +979,6 @@ namespace ts.codefix {
}
}
function isImportableFile(
program: Program,
from: SourceFile,
to: SourceFile,
moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost
) {
const getCanonicalFileName = hostGetCanonicalFileName(moduleSpecifierResolutionHost);
const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.();
return !!moduleSpecifiers.forEachFileNameOfModule(
from.fileName,
to.fileName,
moduleSpecifierResolutionHost,
/*preferSymlinks*/ false,
toPath => {
const toFile = program.getSourceFile(toPath);
// Determine to import using toPath only if toPath is what we were looking at
// or there doesnt exist the file in the program by the symlink
return (toFile === to || !toFile) &&
isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache);
}
);
}
/**
* Don't include something from a `node_modules` that isn't actually reachable by a global import.
* A relative import to node_modules is usually a bad idea.
*/
function isImportablePath(fromPath: string, toPath: string, getCanonicalFileName: GetCanonicalFileName, globalCachePath?: string): boolean {
// If it's in a `node_modules` but is not reachable from here via a global import, don't bother.
const toNodeModules = forEachAncestorDirectory(toPath, ancestor => getBaseFileName(ancestor) === "node_modules" ? ancestor : undefined);
const toNodeModulesParent = toNodeModules && getDirectoryPath(getCanonicalFileName(toNodeModules));
return toNodeModulesParent === undefined
|| startsWith(getCanonicalFileName(fromPath), toNodeModulesParent)
|| (!!globalCachePath && startsWith(getCanonicalFileName(globalCachePath), toNodeModulesParent));
}
export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined): string {
return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target);
}
@@ -1005,117 +1009,4 @@ namespace ts.codefix {
// Need `|| "_"` to ensure result isn't empty.
return !isStringANonContextualKeyword(res) ? res || "_" : `_${res}`;
}
function createAutoImportFilter(fromFile: SourceFile, program: Program, host: LanguageServiceHost, moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host)) {
const packageJsons = (
(host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host)
).filter(p => p.parseable);
let usesNodeCoreModules: boolean | undefined;
return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier, moduleSpecifierResolutionHost };
function moduleSpecifierIsCoveredByPackageJson(specifier: string) {
const packageName = getNodeModuleRootSpecifier(specifier);
for (const packageJson of packageJsons) {
if (packageJson.has(packageName) || packageJson.has(getTypesPackageName(packageName))) {
return true;
}
}
return false;
}
function allowsImportingAmbientModule(moduleSymbol: Symbol): boolean {
if (!packageJsons.length || !moduleSymbol.valueDeclaration) {
return true;
}
const declaringSourceFile = moduleSymbol.valueDeclaration.getSourceFile();
const declaringNodeModuleName = getNodeModulesPackageNameFromFileName(declaringSourceFile.fileName);
if (typeof declaringNodeModuleName === "undefined") {
return true;
}
const declaredModuleSpecifier = stripQuotes(moduleSymbol.getName());
if (isAllowedCoreNodeModulesImport(declaredModuleSpecifier)) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(declaringNodeModuleName)
|| moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier);
}
function allowsImportingSourceFile(sourceFile: SourceFile): boolean {
if (!packageJsons.length) {
return true;
}
const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName);
if (!moduleSpecifier) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier);
}
/**
* Use for a specific module specifier that has already been resolved.
* Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve
* the best module specifier for a given module _and_ determine if it’s importable.
*/
function allowsImportingSpecifier(moduleSpecifier: string) {
if (!packageJsons.length || isAllowedCoreNodeModulesImport(moduleSpecifier)) {
return true;
}
if (pathIsRelative(moduleSpecifier) || isRootedDiskPath(moduleSpecifier)) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier);
}
function isAllowedCoreNodeModulesImport(moduleSpecifier: string) {
// If we’re in JavaScript, it can be difficult to tell whether the user wants to import
// from Node core modules or not. We can start by seeing if the user is actually using
// any node core modules, as opposed to simply having @types/node accidentally as a
// dependency of a dependency.
if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) {
if (usesNodeCoreModules === undefined) {
usesNodeCoreModules = consumesNodeCoreModules(fromFile);
}
if (usesNodeCoreModules) {
return true;
}
}
return false;
}
function getNodeModulesPackageNameFromFileName(importedFileName: string): string | undefined {
if (!stringContains(importedFileName, "node_modules")) {
return undefined;
}
const specifier = moduleSpecifiers.getNodeModulesPackageName(
host.getCompilationSettings(),
fromFile.path,
importedFileName,
moduleSpecifierResolutionHost,
);
if (!specifier) {
return undefined;
}
// Paths here are not node_modules, so we don’t care about them;
// returning anything will trigger a lookup in package.json.
if (!pathIsRelative(specifier) && !isRootedDiskPath(specifier)) {
return getNodeModuleRootSpecifier(specifier);
}
}
function getNodeModuleRootSpecifier(fullSpecifier: string): string {
const components = getPathComponents(getPackageNameFromTypesPackageName(fullSpecifier)).slice(1);
// Scoped packages
if (startsWith(components[0], "@")) {
return `${components[0]}/${components[1]}`;
}
return components[0];
}
}
}
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -8,11 +8,11 @@ namespace ts.Completions.StringCompletions {
if (isInString(sourceFile, position, contextToken)) {
if (!contextToken || !isStringLiteralLike(contextToken)) return undefined;
const entries = getStringLiteralCompletionEntries(sourceFile, contextToken, position, checker, options, host, preferences);
return convertStringLiteralCompletions(entries, contextToken, sourceFile, checker, log, preferences);
return convertStringLiteralCompletions(entries, contextToken, sourceFile, checker, log, options, preferences);
}
}
function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: StringLiteralLike, sourceFile: SourceFile, checker: TypeChecker, log: Log, preferences: UserPreferences): CompletionInfo | undefined {
function convertStringLiteralCompletions(completion: StringLiteralCompletion | undefined, contextToken: StringLiteralLike, sourceFile: SourceFile, checker: TypeChecker, log: Log, options: CompilerOptions, preferences: UserPreferences): CompletionInfo | undefined {
if (completion === undefined) {
return undefined;
}
@@ -33,7 +33,8 @@ namespace ts.Completions.StringCompletions {
ScriptTarget.ESNext,
log,
CompletionKind.String,
preferences
preferences,
options,
); // Target will not be used, so arbitrary
return { isGlobalCompletion: false, isMemberCompletion: true, isNewIdentifierLocation: completion.hasIndexSignature, optionalReplacementSpan, entries };
}
+16 -3
View File
@@ -305,7 +305,9 @@ namespace ts {
/* @internal */
getPackageJsonsForAutoImport?(rootDir?: string): readonly PackageJsonInfo[];
/* @internal */
getImportSuggestionsCache?(): Completions.ImportSuggestionsForFileCache;
getExportMapCache?(): ExportMapCache;
/* @internal */
getModuleSpecifierCache?(): ModuleSpecifierCache;
/* @internal */
setCompilerHost?(host: CompilerHost): void;
/* @internal */
@@ -552,7 +554,7 @@ namespace ts {
export type OrganizeImportsScope = CombinedCodeFixScope;
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#";
export type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
export interface GetCompletionsAtPositionOptions extends UserPreferences {
/**
@@ -1140,11 +1142,14 @@ namespace ts {
* must be used to commit that completion entry.
*/
optionalReplacementSpan?: TextSpan;
/**
* true when the current location also allows for a new identifier
*/
isNewIdentifierLocation: boolean;
/**
* Indicates to client to continue requesting completions on subsequent keystrokes.
*/
isIncomplete?: true;
entries: CompletionEntry[];
}
@@ -1160,6 +1165,10 @@ namespace ts {
* in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default.
*/
exportName: string;
/**
* Set for auto imports with eagerly resolved module specifiers.
*/
moduleSpecifier?: string;
}
// see comments in protocol.ts
@@ -1169,6 +1178,7 @@ namespace ts {
kindModifiers?: string; // see ScriptElementKindModifier, comma separated
sortText: string;
insertText?: string;
isSnippet?: true;
/**
* An optional span that indicates the text to be replaced by this completion item.
* If present, this span should be used instead of the default one.
@@ -1177,6 +1187,7 @@ namespace ts {
replacementSpan?: TextSpan;
hasAction?: true;
source?: string;
sourceDisplay?: SymbolDisplayPart[];
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
@@ -1199,7 +1210,9 @@ namespace ts {
documentation?: SymbolDisplayPart[];
tags?: JSDocTagInfo[];
codeActions?: CodeAction[];
/** @deprecated Use `sourceDisplay` instead. */
source?: SymbolDisplayPart[];
sourceDisplay?: SymbolDisplayPart[];
}
export interface OutliningSpan {
+327 -1
View File
@@ -1821,7 +1821,7 @@ namespace ts {
}
export function createModuleSpecifierResolutionHost(program: Program, host: LanguageServiceHost): ModuleSpecifierResolutionHost {
// Mix in `getProbableSymlinks` from Program when host doesn't have it
// Mix in `getSymlinkCache` from Program when host doesn't have it
// in order for non-Project hosts to have a symlinks cache.
return {
fileExists: fileName => program.fileExists(fileName),
@@ -1829,6 +1829,7 @@ namespace ts {
readFile: maybeBind(host, host.readFile),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
getSymlinkCache: maybeBind(host, host.getSymlinkCache) || program.getSymlinkCache,
getModuleSpecifierCache: maybeBind(host, host.getModuleSpecifierCache),
getGlobalTypingsCacheLocation: maybeBind(host, host.getGlobalTypingsCacheLocation),
getSourceFiles: () => program.getSourceFiles(),
redirectTargetsMap: program.redirectTargetsMap,
@@ -2846,6 +2847,125 @@ namespace ts {
}
}
export interface PackageJsonImportFilter {
allowsImportingAmbientModule: (moduleSymbol: Symbol, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost) => boolean;
allowsImportingSourceFile: (sourceFile: SourceFile, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost) => boolean;
/**
* Use for a specific module specifier that has already been resolved.
* Use `allowsImportingAmbientModule` or `allowsImportingSourceFile` to resolve
* the best module specifier for a given module _and_ determine if it’s importable.
*/
allowsImportingSpecifier: (moduleSpecifier: string) => boolean;
}
export function createPackageJsonImportFilter(fromFile: SourceFile, host: LanguageServiceHost): PackageJsonImportFilter {
const packageJsons = (
(host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host)
).filter(p => p.parseable);
let usesNodeCoreModules: boolean | undefined;
return { allowsImportingAmbientModule, allowsImportingSourceFile, allowsImportingSpecifier };
function moduleSpecifierIsCoveredByPackageJson(specifier: string) {
const packageName = getNodeModuleRootSpecifier(specifier);
for (const packageJson of packageJsons) {
if (packageJson.has(packageName) || packageJson.has(getTypesPackageName(packageName))) {
return true;
}
}
return false;
}
function allowsImportingAmbientModule(moduleSymbol: Symbol, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost): boolean {
if (!packageJsons.length || !moduleSymbol.valueDeclaration) {
return true;
}
const declaringSourceFile = moduleSymbol.valueDeclaration.getSourceFile();
const declaringNodeModuleName = getNodeModulesPackageNameFromFileName(declaringSourceFile.fileName, moduleSpecifierResolutionHost);
if (typeof declaringNodeModuleName === "undefined") {
return true;
}
const declaredModuleSpecifier = stripQuotes(moduleSymbol.getName());
if (isAllowedCoreNodeModulesImport(declaredModuleSpecifier)) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(declaringNodeModuleName)
|| moduleSpecifierIsCoveredByPackageJson(declaredModuleSpecifier);
}
function allowsImportingSourceFile(sourceFile: SourceFile, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost): boolean {
if (!packageJsons.length) {
return true;
}
const moduleSpecifier = getNodeModulesPackageNameFromFileName(sourceFile.fileName, moduleSpecifierResolutionHost);
if (!moduleSpecifier) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier);
}
function allowsImportingSpecifier(moduleSpecifier: string) {
if (!packageJsons.length || isAllowedCoreNodeModulesImport(moduleSpecifier)) {
return true;
}
if (pathIsRelative(moduleSpecifier) || isRootedDiskPath(moduleSpecifier)) {
return true;
}
return moduleSpecifierIsCoveredByPackageJson(moduleSpecifier);
}
function isAllowedCoreNodeModulesImport(moduleSpecifier: string) {
// If we’re in JavaScript, it can be difficult to tell whether the user wants to import
// from Node core modules or not. We can start by seeing if the user is actually using
// any node core modules, as opposed to simply having @types/node accidentally as a
// dependency of a dependency.
if (isSourceFileJS(fromFile) && JsTyping.nodeCoreModules.has(moduleSpecifier)) {
if (usesNodeCoreModules === undefined) {
usesNodeCoreModules = consumesNodeCoreModules(fromFile);
}
if (usesNodeCoreModules) {
return true;
}
}
return false;
}
function getNodeModulesPackageNameFromFileName(importedFileName: string, moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost): string | undefined {
if (!stringContains(importedFileName, "node_modules")) {
return undefined;
}
const specifier = moduleSpecifiers.getNodeModulesPackageName(
host.getCompilationSettings(),
fromFile.path,
importedFileName,
moduleSpecifierResolutionHost,
);
if (!specifier) {
return undefined;
}
// Paths here are not node_modules, so we don’t care about them;
// returning anything will trigger a lookup in package.json.
if (!pathIsRelative(specifier) && !isRootedDiskPath(specifier)) {
return getNodeModuleRootSpecifier(specifier);
}
}
function getNodeModuleRootSpecifier(fullSpecifier: string): string {
const components = getPathComponents(getPackageNameFromTypesPackageName(fullSpecifier)).slice(1);
// Scoped packages
if (startsWith(components[0], "@")) {
return `${components[0]}/${components[1]}`;
}
return components[0];
}
}
function tryParseJson(text: string) {
try {
return JSON.parse(text);
@@ -2994,5 +3114,211 @@ namespace ts {
return isInJSFile(declaration) || !findAncestor(declaration, isGlobalScopeAugmentation);
}
export const enum ImportKind {
Named,
Default,
Namespace,
CommonJS,
}
export const enum ExportKind {
Named,
Default,
ExportEquals,
UMD,
}
/** Information about how a symbol is exported from a module. */
export interface SymbolExportInfo {
symbol: Symbol;
moduleSymbol: Symbol;
exportKind: ExportKind;
/** If true, can't use an es6 import from a js file. */
exportedSymbolIsTypeOnly: boolean;
/** True if export was only found via the package.json AutoImportProvider (for telemetry). */
isFromPackageJson: boolean;
}
export interface ExportMapCache {
clear(): void;
get(file: Path, checker: TypeChecker, projectVersion?: string): MultiMap<string, SymbolExportInfo> | undefined;
set(suggestions: MultiMap<string, SymbolExportInfo>, projectVersion?: string): void;
isEmpty(): boolean;
/** @returns Whether the change resulted in the cache being cleared */
onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean): boolean;
}
export function createExportMapCache(): ExportMapCache {
let cache: MultiMap<string, SymbolExportInfo> | undefined;
let projectVersion: string | undefined;
let usableByFileName: Path | undefined;
const wrapped: ExportMapCache = {
isEmpty() {
return !cache;
},
clear() {
cache = undefined;
projectVersion = undefined;
},
set(suggestions, version) {
cache = suggestions;
if (version) {
projectVersion = version;
}
},
get: (file, checker, version) => {
if (usableByFileName && file !== usableByFileName) {
return undefined;
}
if (version && projectVersion === version) {
return cache;
}
cache?.forEach(infos => {
for (const info of infos) {
// If the symbol/moduleSymbol was a merged symbol, it will have a new identity
// in the checker, even though the symbols to merge are the same (guaranteed by
// cache invalidation in synchronizeHostData).
if (info.symbol.declarations?.length) {
info.symbol = checker.getMergedSymbol(info.exportKind === ExportKind.Default
? info.symbol.declarations[0].localSymbol ?? info.symbol.declarations[0].symbol
: info.symbol.declarations[0].symbol);
}
if (info.moduleSymbol.declarations?.length) {
info.moduleSymbol = checker.getMergedSymbol(info.moduleSymbol.declarations[0].symbol);
}
}
});
return cache;
},
onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean) {
if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) {
// File is purely global; doesn't affect export map
return false;
}
if (
usableByFileName && usableByFileName !== newSourceFile.path ||
// If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node.
// Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list.
typeAcquisitionEnabled && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile) ||
// Module agumentation and ambient module changes can add or remove exports available to be auto-imported.
// Changes elsewhere in the file can change the *type* of an export in a module augmentation,
// but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache.
!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) ||
!ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile)
) {
this.clear();
return true;
}
usableByFileName = newSourceFile.path;
return false;
},
};
if (Debug.isDebugging) {
Object.defineProperty(wrapped, "__cache", { get: () => cache });
}
return wrapped;
function fileIsGlobalOnly(file: SourceFile) {
return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames;
}
function ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) {
if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) {
return false;
}
let oldFileStatementIndex = -1;
let newFileStatementIndex = -1;
for (const ambientModuleName of newSourceFile.ambientModuleNames) {
const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName;
oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1);
newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1);
if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) {
return false;
}
}
return true;
}
}
export function createModuleSpecifierCache(): ModuleSpecifierCache {
let cache: ESMap<Path, boolean | readonly ModulePath[]> | undefined;
let importingFileName: Path | undefined;
const wrapped: ModuleSpecifierCache = {
get(fromFileName, toFileName) {
if (!cache || fromFileName !== importingFileName) return undefined;
return cache.get(toFileName);
},
set(fromFileName, toFileName, moduleSpecifiers) {
if (cache && fromFileName !== importingFileName) {
cache.clear();
}
importingFileName = fromFileName;
(cache ||= new Map()).set(toFileName, moduleSpecifiers);
},
clear() {
cache = undefined;
importingFileName = undefined;
},
count() {
return cache ? cache.size : 0;
}
};
if (Debug.isDebugging) {
Object.defineProperty(wrapped, "__cache", { get: () => cache });
}
return wrapped;
}
export function isImportableFile(
program: Program,
from: SourceFile,
to: SourceFile,
packageJsonFilter: PackageJsonImportFilter | undefined,
moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost,
moduleSpecifierCache: ModuleSpecifierCache | undefined,
): boolean {
if (from === to) return false;
const cachedResult = moduleSpecifierCache?.get(from.path, to.path);
if (cachedResult !== undefined) {
return !!cachedResult;
}
const getCanonicalFileName = hostGetCanonicalFileName(moduleSpecifierResolutionHost);
const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.();
const hasImportablePath = !!moduleSpecifiers.forEachFileNameOfModule(
from.fileName,
to.fileName,
moduleSpecifierResolutionHost,
/*preferSymlinks*/ false,
toPath => {
const toFile = program.getSourceFile(toPath);
// Determine to import using toPath only if toPath is what we were looking at
// or there doesnt exist the file in the program by the symlink
return (toFile === to || !toFile) &&
isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache);
}
);
if (packageJsonFilter) {
const isImportable = hasImportablePath && packageJsonFilter.allowsImportingSourceFile(to, moduleSpecifierResolutionHost);
moduleSpecifierCache?.set(from.path, to.path, isImportable);
return isImportable;
}
return hasImportablePath;
}
/**
* Don't include something from a `node_modules` that isn't actually reachable by a global import.
* A relative import to node_modules is usually a bad idea.
*/
function isImportablePath(fromPath: string, toPath: string, getCanonicalFileName: GetCanonicalFileName, globalCachePath?: string): boolean {
// If it's in a `node_modules` but is not reachable from here via a global import, don't bother.
const toNodeModules = forEachAncestorDirectory(toPath, ancestor => getBaseFileName(ancestor) === "node_modules" ? ancestor : undefined);
const toNodeModulesParent = toNodeModules && getDirectoryPath(getCanonicalFileName(toNodeModules));
return toNodeModulesParent === undefined
|| startsWith(getCanonicalFileName(fromPath), toNodeModulesParent)
|| (!!globalCachePath && startsWith(getCanonicalFileName(globalCachePath), toNodeModulesParent));
}
// #endregion
}
+2 -1
View File
@@ -169,6 +169,7 @@
"unittests/tsserver/events/projectLanguageServiceState.ts",
"unittests/tsserver/events/projectLoading.ts",
"unittests/tsserver/events/projectUpdatedInBackground.ts",
"unittests/tsserver/exportMapCache.ts",
"unittests/tsserver/externalProjects.ts",
"unittests/tsserver/forceConsistentCasingInFileNames.ts",
"unittests/tsserver/formatSettings.ts",
@@ -177,12 +178,12 @@
"unittests/tsserver/getExportReferences.ts",
"unittests/tsserver/getFileReferences.ts",
"unittests/tsserver/importHelpers.ts",
"unittests/tsserver/importSuggestionsCache.ts",
"unittests/tsserver/inferredProjects.ts",
"unittests/tsserver/jsdocTag.ts",
"unittests/tsserver/languageService.ts",
"unittests/tsserver/maxNodeModuleJsDepth.ts",
"unittests/tsserver/metadataInResponse.ts",
"unittests/tsserver/moduleSpecifierCache.ts",
"unittests/tsserver/navTo.ts",
"unittests/tsserver/occurences.ts",
"unittests/tsserver/openFile.ts",
@@ -39,7 +39,9 @@ namespace ts.projectSystem {
isPackageJsonImport: undefined,
sortText: Completions.SortText.AutoImportSuggestions,
source: "/a",
data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined }
sourceDisplay: undefined,
isSnippet: undefined,
data: { exportName: "foo", fileName: "/a.ts", ambientModuleName: undefined, isPackageJsonImport: undefined, moduleSpecifier: undefined }
};
assert.deepEqual<protocol.CompletionInfo | undefined>(response, {
isGlobalCompletion: true,
@@ -69,6 +71,7 @@ namespace ts.projectSystem {
kindModifiers: ScriptElementKindModifier.exportedModifier,
name: "foo",
source: [{ text: "./a", kind: "text" }],
sourceDisplay: [{ text: "./a", kind: "text" }],
};
assert.deepEqual<readonly protocol.CompletionEntryDetails[] | undefined>(detailsResponse, [
{
@@ -0,0 +1,88 @@
namespace ts.projectSystem {
const packageJson: File = {
path: "/package.json",
content: `{ "dependencies": { "mobx": "*" } }`
};
const aTs: File = {
path: "/a.ts",
content: "export const foo = 0;",
};
const bTs: File = {
path: "/b.ts",
content: "foo",
};
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const ambientDeclaration: File = {
path: "/ambient.d.ts",
content: "declare module 'ambient' {}"
};
const mobxDts: File = {
path: "/node_modules/mobx/index.d.ts",
content: "export declare function observable(): unknown;"
};
describe("unittests:: tsserver:: exportMapCache", () => {
it("caches auto-imports in the same file", () => {
const { exportMapCache, checker } = setup();
assert.ok(exportMapCache.get(bTs.path as Path, checker));
});
it("invalidates the cache when new files are added", () => {
const { host, exportMapCache, checker } = setup();
host.writeFile("/src/a2.ts", aTs.content);
host.runQueuedTimeoutCallbacks();
assert.isUndefined(exportMapCache.get(bTs.path as Path, checker));
});
it("invalidates the cache when files are deleted", () => {
const { host, projectService, exportMapCache, checker } = setup();
projectService.closeClientFile(aTs.path);
host.deleteFile(aTs.path);
host.runQueuedTimeoutCallbacks();
assert.isUndefined(exportMapCache.get(bTs.path as Path, checker));
});
it("does not invalidate the cache when package.json is changed inconsequentially", () => {
const { host, exportMapCache, checker, project } = setup();
host.writeFile("/package.json", `{ "name": "blah", "dependencies": { "mobx": "*" } }`);
host.runQueuedTimeoutCallbacks();
project.getPackageJsonAutoImportProvider();
assert.ok(exportMapCache.get(bTs.path as Path, checker));
});
it("invalidates the cache when package.json change results in AutoImportProvider change", () => {
const { host, exportMapCache, checker, project } = setup();
host.writeFile("/package.json", `{}`);
host.runQueuedTimeoutCallbacks();
project.getPackageJsonAutoImportProvider();
assert.isUndefined(exportMapCache.get(bTs.path as Path, checker));
});
});
function setup() {
const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxDts]);
const session = createSession(host);
openFilesForSession([aTs, bTs], session);
const projectService = session.getProjectService();
const project = configuredProjectAt(projectService, 0);
triggerCompletions();
const checker = project.getLanguageService().getProgram()!.getTypeChecker();
return { host, project, projectService, exportMapCache: project.getExportMapCache(), checker, triggerCompletions };
function triggerCompletions() {
const requestLocation: protocol.FileLocationRequestArgs = {
file: bTs.path,
line: 1,
offset: 3,
};
executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
...requestLocation,
includeExternalModuleExports: true,
prefix: "foo",
});
}
}
}
@@ -1,75 +0,0 @@
namespace ts.projectSystem {
const packageJson: File = {
path: "/package.json",
content: `{ "dependencies": { "mobx": "*" } }`
};
const aTs: File = {
path: "/a.ts",
content: "export const foo = 0;",
};
const bTs: File = {
path: "/b.ts",
content: "foo",
};
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const ambientDeclaration: File = {
path: "/ambient.d.ts",
content: "declare module 'ambient' {}"
};
const mobxDts: File = {
path: "/node_modules/mobx/index.d.ts",
content: "export declare function observable(): unknown;"
};
describe("unittests:: tsserver:: importSuggestionsCache", () => {
it("caches auto-imports in the same file", () => {
const { importSuggestionsCache, checker } = setup();
assert.ok(importSuggestionsCache.get(bTs.path, checker));
});
it("invalidates the cache when new files are added", () => {
const { host, importSuggestionsCache, checker } = setup();
host.writeFile("/src/a2.ts", aTs.content);
host.runQueuedTimeoutCallbacks();
assert.isUndefined(importSuggestionsCache.get(bTs.path, checker));
});
it("invalidates the cache when files are deleted", () => {
const { host, projectService, importSuggestionsCache, checker } = setup();
projectService.closeClientFile(aTs.path);
host.deleteFile(aTs.path);
host.runQueuedTimeoutCallbacks();
assert.isUndefined(importSuggestionsCache.get(bTs.path, checker));
});
it("invalidates the cache when package.json is changed", () => {
const { host, importSuggestionsCache, checker } = setup();
host.writeFile("/package.json", "{}");
host.runQueuedTimeoutCallbacks();
assert.isUndefined(importSuggestionsCache.get(bTs.path, checker));
});
});
function setup() {
const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxDts]);
const session = createSession(host);
openFilesForSession([aTs, bTs], session);
const projectService = session.getProjectService();
const project = configuredProjectAt(projectService, 0);
const requestLocation: protocol.FileLocationRequestArgs = {
file: bTs.path,
line: 1,
offset: 3,
};
executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
...requestLocation,
includeExternalModuleExports: true,
prefix: "foo",
});
const checker = project.getLanguageService().getProgram()!.getTypeChecker();
return { host, project, projectService, importSuggestionsCache: project.getImportSuggestionsCache(), checker };
}
}
@@ -592,6 +592,7 @@ foo`
kindModifiers: "",
name: "foo",
source: undefined,
sourceDisplay: undefined,
tags,
}]);
}
@@ -0,0 +1,81 @@
namespace ts.projectSystem {
const packageJson: File = {
path: "/package.json",
content: `{ "dependencies": { "mobx": "*" } }`
};
const aTs: File = {
path: "/a.ts",
content: "export const foo = 0;",
};
const bTs: File = {
path: "/b.ts",
content: "foo",
};
const bSymlink: SymLink = {
path: "/b-link.ts",
symLink: "./b.ts",
};
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const ambientDeclaration: File = {
path: "/ambient.d.ts",
content: "declare module 'ambient' {}"
};
const mobxDts: File = {
path: "/node_modules/mobx/index.d.ts",
content: "export declare function observable(): unknown;"
};
describe("unittests:: tsserver:: moduleSpecifierCache", () => {
it("caches importability within a file", () => {
const { moduleSpecifierCache } = setup();
assert.isTrue(moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path));
});
it("does not invalidate the cache when new files are added", () => {
const { host, moduleSpecifierCache } = setup();
host.writeFile("/src/a2.ts", aTs.content);
host.runQueuedTimeoutCallbacks();
assert.isTrue(moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path));
});
it("invalidates the cache when symlinks are added or removed", () => {
const { host, moduleSpecifierCache } = setup();
host.renameFile(bSymlink.path, "/b-link2.ts");
host.runQueuedTimeoutCallbacks();
assert.equal(moduleSpecifierCache.count(), 0);
});
it("invalidates the cache when package.json changes", () => {
const { host, moduleSpecifierCache } = setup();
host.writeFile("/package.json", `{}`);
host.runQueuedTimeoutCallbacks();
assert.isUndefined(moduleSpecifierCache.get(bTs.path as Path, aTs.path as Path));
});
});
function setup() {
const host = createServerHost([aTs, bTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxDts]);
const session = createSession(host);
openFilesForSession([aTs, bTs], session);
const projectService = session.getProjectService();
const project = configuredProjectAt(projectService, 0);
triggerCompletions();
return { host, project, projectService, moduleSpecifierCache: project.getModuleSpecifierCache(), triggerCompletions };
function triggerCompletions() {
const requestLocation: protocol.FileLocationRequestArgs = {
file: bTs.path,
line: 1,
offset: 3,
};
executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
...requestLocation,
includeExternalModuleExports: true,
prefix: "foo",
});
}
}
}
@@ -72,6 +72,8 @@ import { something } from "something";
replacementSpan: undefined,
source: undefined,
data: undefined,
sourceDisplay: undefined,
isSnippet: undefined,
};
}
});
+39 -4
View File
@@ -3905,6 +3905,8 @@ declare namespace ts {
readonly disableSuggestions?: boolean;
readonly quotePreference?: "auto" | "double" | "single";
readonly includeCompletionsForModuleExports?: boolean;
readonly includeCompletionsForImportStatements?: boolean;
readonly includeCompletionsWithSnippetText?: boolean;
readonly includeAutomaticOptionalChainCompletions?: boolean;
readonly includeCompletionsWithInsertText?: boolean;
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
@@ -5654,7 +5656,7 @@ declare namespace ts {
fileName: string;
}
type OrganizeImportsScope = CombinedCodeFixScope;
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#";
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
interface GetCompletionsAtPositionOptions extends UserPreferences {
/**
* If the editor is asking for completions because a certain character was typed
@@ -6140,6 +6142,10 @@ declare namespace ts {
* true when the current location also allows for a new identifier
*/
isNewIdentifierLocation: boolean;
/**
* Indicates to client to continue requesting completions on subsequent keystrokes.
*/
isIncomplete?: true;
entries: CompletionEntry[];
}
interface CompletionEntryData {
@@ -6154,6 +6160,10 @@ declare namespace ts {
* in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default.
*/
exportName: string;
/**
* Set for auto imports with eagerly resolved module specifiers.
*/
moduleSpecifier?: string;
}
interface CompletionEntry {
name: string;
@@ -6161,6 +6171,7 @@ declare namespace ts {
kindModifiers?: string;
sortText: string;
insertText?: string;
isSnippet?: true;
/**
* An optional span that indicates the text to be replaced by this completion item.
* If present, this span should be used instead of the default one.
@@ -6169,6 +6180,7 @@ declare namespace ts {
replacementSpan?: TextSpan;
hasAction?: true;
source?: string;
sourceDisplay?: SymbolDisplayPart[];
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
@@ -6190,7 +6202,9 @@ declare namespace ts {
documentation?: SymbolDisplayPart[];
tags?: JSDocTagInfo[];
codeActions?: CodeAction[];
/** @deprecated Use `sourceDisplay` instead. */
source?: SymbolDisplayPart[];
sourceDisplay?: SymbolDisplayPart[];
}
interface OutliningSpan {
/** The span of the document to actually collapse. */
@@ -8214,7 +8228,7 @@ declare namespace ts.server.protocol {
command: CommandTypes.Formatonkey;
arguments: FormatOnKeyRequestArgs;
}
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#";
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
/**
* Arguments for completions messages.
*/
@@ -8316,6 +8330,10 @@ declare namespace ts.server.protocol {
* coupled with `replacementSpan` to replace a dotted access with a bracket access.
*/
insertText?: string;
/**
* `insertText` should be interpreted as a snippet if true.
*/
isSnippet?: true;
/**
* An optional span that indicates the text to be replaced by this completion item.
* If present, this span should be used instead of the default one.
@@ -8331,6 +8349,10 @@ declare namespace ts.server.protocol {
* Identifier (not necessarily human-readable) identifying where this completion came from.
*/
source?: string;
/**
* Human-readable description of the `source`.
*/
sourceDisplay?: SymbolDisplayPart[];
/**
* If true, this completion should be highlighted as recommended. There will only be one of these.
* This will be set when we know the user should write an expression with a certain type and that type is an enum or constructable class.
@@ -8387,9 +8409,13 @@ declare namespace ts.server.protocol {
*/
codeActions?: CodeAction[];
/**
* Human-readable description of the `source` from the CompletionEntry.
* @deprecated Use `sourceDisplay` instead.
*/
source?: SymbolDisplayPart[];
/**
* Human-readable description of the `source` from the CompletionEntry.
*/
sourceDisplay?: SymbolDisplayPart[];
}
/** @deprecated Prefer CompletionInfoResponse, which supports several top-level fields in addition to the array of entries. */
interface CompletionsResponse extends Response {
@@ -8408,6 +8434,7 @@ declare namespace ts.server.protocol {
* must be used to commit that completion entry.
*/
readonly optionalReplacementSpan?: TextSpan;
readonly isIncomplete?: boolean;
readonly entries: readonly CompletionEntry[];
}
interface CompletionDetailsResponse extends Response {
@@ -9190,6 +9217,15 @@ declare namespace ts.server.protocol {
* This affects lone identifier completions but not completions on the right hand side of `obj.`.
*/
readonly includeCompletionsForModuleExports?: boolean;
/**
* Enables auto-import-style completions on partially-typed import statements. E.g., allows
* `import write|` to be completed to `import { writeFile } from "fs"`.
*/
readonly includeCompletionsForImportStatements?: boolean;
/**
* Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`.
*/
readonly includeCompletionsWithSnippetText?: boolean;
/**
* If enabled, the completion list will include completions with invalid identifier names.
* For those entries, The `insertText` and `replacementSpan` properties will be set to change from `.x` property access to `["x"]`.
@@ -9617,7 +9653,6 @@ declare namespace ts.server {
markAsDirty(): void;
getScriptFileNames(): string[];
getLanguageService(): never;
markAutoImportProviderAsDirty(): never;
getModuleResolutionHostForAutoImportProvider(): never;
getProjectReferences(): readonly ProjectReference[] | undefined;
getTypeAcquisition(): TypeAcquisition;
+15 -1
View File
@@ -3905,6 +3905,8 @@ declare namespace ts {
readonly disableSuggestions?: boolean;
readonly quotePreference?: "auto" | "double" | "single";
readonly includeCompletionsForModuleExports?: boolean;
readonly includeCompletionsForImportStatements?: boolean;
readonly includeCompletionsWithSnippetText?: boolean;
readonly includeAutomaticOptionalChainCompletions?: boolean;
readonly includeCompletionsWithInsertText?: boolean;
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
@@ -5654,7 +5656,7 @@ declare namespace ts {
fileName: string;
}
type OrganizeImportsScope = CombinedCodeFixScope;
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#";
type CompletionsTriggerCharacter = "." | '"' | "'" | "`" | "/" | "@" | "<" | "#" | " ";
interface GetCompletionsAtPositionOptions extends UserPreferences {
/**
* If the editor is asking for completions because a certain character was typed
@@ -6140,6 +6142,10 @@ declare namespace ts {
* true when the current location also allows for a new identifier
*/
isNewIdentifierLocation: boolean;
/**
* Indicates to client to continue requesting completions on subsequent keystrokes.
*/
isIncomplete?: true;
entries: CompletionEntry[];
}
interface CompletionEntryData {
@@ -6154,6 +6160,10 @@ declare namespace ts {
* in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default.
*/
exportName: string;
/**
* Set for auto imports with eagerly resolved module specifiers.
*/
moduleSpecifier?: string;
}
interface CompletionEntry {
name: string;
@@ -6161,6 +6171,7 @@ declare namespace ts {
kindModifiers?: string;
sortText: string;
insertText?: string;
isSnippet?: true;
/**
* An optional span that indicates the text to be replaced by this completion item.
* If present, this span should be used instead of the default one.
@@ -6169,6 +6180,7 @@ declare namespace ts {
replacementSpan?: TextSpan;
hasAction?: true;
source?: string;
sourceDisplay?: SymbolDisplayPart[];
isRecommended?: true;
isFromUncheckedFile?: true;
isPackageJsonImport?: true;
@@ -6190,7 +6202,9 @@ declare namespace ts {
documentation?: SymbolDisplayPart[];
tags?: JSDocTagInfo[];
codeActions?: CodeAction[];
/** @deprecated Use `sourceDisplay` instead. */
source?: SymbolDisplayPart[];
sourceDisplay?: SymbolDisplayPart[];
}
interface OutliningSpan {
/** The span of the document to actually collapse. */
@@ -1,22 +0,0 @@
///<reference path="fourslash.ts" />
////import /*1*/ /*2*/
verify.completions({ marker: "1", exact: undefined });
edit.insert('q');
verify.completions({ exact: undefined });
verifyIncompleteImportName();
goTo.marker('2');
edit.insert(" = ");
verifyIncompleteImportName();
goTo.marker("2");
edit.moveRight(" = ".length);
edit.insert("a.");
verifyIncompleteImportName();
function verifyIncompleteImportName() {
verify.completions({ marker: "1", exact: undefined });
verify.quickInfoIs("import q");
}
@@ -18,8 +18,8 @@ verify.completions({
name: "someModule",
source: "/someModule",
sourceDisplay: "./someModule",
text: "const someModule: 0",
kind: "const",
text: "(property) default: 1",
kind: "property",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
@@ -28,8 +28,8 @@ verify.completions({
name: "someModule",
source: "/someModule",
sourceDisplay: "./someModule",
text: "(property) default: 1",
kind: "property",
text: "const someModule: 0",
kind: "const",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
@@ -4,10 +4,13 @@
////export const abc = 0;
// @Filename: /b.ts
////acb/**/;
////acb/*1*/;
goTo.marker("");
verify.applyCodeActionFromCompletion("", {
// @Filename: /c.ts
////acb/*2*/;
goTo.marker("1");
verify.applyCodeActionFromCompletion("1", {
name: "abc",
source: "/a",
description: `Import 'abc' from module "./a"`,
@@ -15,3 +18,17 @@ verify.applyCodeActionFromCompletion("", {
acb;`,
});
goTo.marker("2");
verify.applyCodeActionFromCompletion("2", {
name: "abc",
source: "/a",
data: {
exportName: "abc",
fileName: "/a.ts",
},
description: `Import 'abc' from module "./a"`,
newFileContent: `import { abc } from "./a";
acb;`,
});
@@ -3,26 +3,28 @@
// @module: es6
// @Filename: /console.d.ts
////interface Console {}
////declare var console: Console;
////declare module "console" {
//// export = console;
////}
//// interface Console {}
//// declare var console: Console;
//// declare module "console" {
//// export = console;
//// }
// @Filename: /react-native.d.ts
//// import 'console';
////declare global {
//// interface Console {}
//// var console: Console;
////}
//// declare global {
//// interface Console {}
//// var console: Console;
//// }
// @Filename: /a.ts
////conso/**/
verify.completions({
marker: "",
exact: completion.globalsPlus([{
hasAction: undefined, // asserts that it does *not* have an action
name: "console"
name: "console",
sortText: completion.SortText.GlobalsOrKeywords,
}]),
preferences: {
includeCompletionsForModuleExports: true,
+3
View File
@@ -621,6 +621,8 @@ declare namespace FourSlashInterface {
interface UserPreferences {
readonly quotePreference?: "auto" | "double" | "single";
readonly includeCompletionsForModuleExports?: boolean;
readonly includeCompletionsForImportStatements?: boolean;
readonly includeCompletionsWithSnippetText?: boolean;
readonly includeInsertTextCompletions?: boolean;
readonly includeAutomaticOptionalChainCompletions?: boolean;
readonly importModuleSpecifierPreference?: "shortest" | "project-relative" | "relative" | "non-relative";
@@ -650,6 +652,7 @@ declare namespace FourSlashInterface {
readonly kindModifiers?: string;
readonly sortText?: completion.SortText;
readonly isPackageJsonImport?: boolean;
readonly isSnippet?: boolean;
// details
readonly text?: string;
@@ -0,0 +1,75 @@
/// <reference path="fourslash.ts" />
// @Filename: /mod.ts
//// export const foo = 0;
// @Filename: /index0.ts
//// [|import f/*0*/|]
// @Filename: /index1.ts
//// [|import { f/*1*/}|]
// @Filename: /index2.ts
//// [|import * as f/*2*/|]
// @Filename: /index3.ts
//// [|import f/*3*/ from|]
// @Filename: /index4.ts
//// [|import f/*4*/ =|]
// @Filename: /index5.ts
//// import f/*5*/ from "";
[0, 1, 2, 3, 4, 5].forEach(marker => {
verify.completions({
marker: "" + marker,
exact: [{
name: "foo",
source: "./mod",
insertText: `import { foo$1 } from "./mod";`,
isSnippet: true,
replacementSpan: test.ranges()[marker],
sourceDisplay: "./mod",
}],
preferences: {
includeCompletionsForImportStatements: true,
includeInsertTextCompletions: true,
includeCompletionsWithSnippetText: true,
}
});
});
// @Filename: /index6.ts
//// import f/*6*/ from "nope";
// @Filename: /index7.ts
//// import { f/*7*/, bar }
// @Filename: /index8.ts
//// import foo, { f/*8*/ }
// @Filename: /index9.ts
//// import g/*9*/
// @Filename: /index10.ts
//// import f/*10*/ from "./mod";
// @Filename: /index11.ts
//// import oo/*11*/
// @Filename: /index12.ts
//// import {
//// /*12*/
//// }
[6, 7, 8, 9, 10, 11, 12].forEach(marker => {
verify.completions({
marker: "" + marker,
exact: [],
preferences: {
includeCompletionsForImportStatements: true,
includeInsertTextCompletions: true,
}
});
});
@@ -0,0 +1,27 @@
/// <reference path="fourslash.ts" />
// @esModuleInterop: false
// @Filename: /mod.ts
//// const foo = 0;
//// export = foo;
// @Filename: /importExportEquals.ts
//// [|import f/**/|]
verify.completions({
marker: "",
exact: [{
name: "foo",
source: "./mod",
insertText: `import foo$1 = require("./mod");`,
isSnippet: true,
replacementSpan: test.ranges()[0],
sourceDisplay: "./mod",
}],
preferences: {
includeCompletionsForImportStatements: true,
includeInsertTextCompletions: true,
includeCompletionsWithSnippetText: true,
}
});
@@ -0,0 +1,27 @@
/// <reference path="fourslash.ts" />
// @esModuleInterop: true
// @Filename: /mod.ts
//// const foo = 0;
//// export = foo;
// @Filename: /importExportEquals.ts
//// [|import f/**/|]
verify.completions({
marker: "",
exact: [{
name: "foo",
source: "./mod",
insertText: `import foo$1 from "./mod";`, // <-- default import
isSnippet: true,
replacementSpan: test.ranges()[0],
sourceDisplay: "./mod",
}],
preferences: {
includeCompletionsForImportStatements: true,
includeInsertTextCompletions: true,
includeCompletionsWithSnippetText: true,
}
});
@@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />
// @Filename: /types.d.ts
//// declare module "*.css" {
//// const styles: any;
//// export = styles;
//// }
// @Filename: /index.ts
//// import style/**/
verify.completions({
marker: "",
exact: [],
preferences: {
includeCompletionsForImportStatements: true,
includeInsertTextCompletions: true,
includeCompletionsWithSnippetText: true,
}
});
@@ -0,0 +1,24 @@
/// <reference path="fourslash.ts" />
// @Filename: /mod.ts
//// export const foo = 0;
// @Filename: /index0.ts
//// [|import f/**/|]
verify.completions({
marker: "",
exact: [{
name: "foo",
source: "./mod",
insertText: `import { foo } from "./mod";`, // <-- no `$1` tab stop
isSnippet: undefined, // <-- undefined
replacementSpan: test.ranges()[0],
sourceDisplay: "./mod",
}],
preferences: {
includeCompletionsForImportStatements: true,
includeInsertTextCompletions: true,
includeCompletionsWithSnippetText: false, // <-- false
}
});
@@ -0,0 +1,25 @@
/// <reference path="fourslash.ts" />
// @Filename: /mod.ts
//// export const foo = 0;
// @Filename: /single.ts
//// import * as fs from 'fs';
//// [|import f/**/|]
verify.completions({
marker: "",
exact: [{
name: "foo",
source: "./mod",
insertText: `import { foo$1 } from './mod';`, // <-- single quotes
isSnippet: true,
replacementSpan: test.ranges()[0],
sourceDisplay: "./mod",
}],
preferences: {
includeCompletionsForImportStatements: true,
includeInsertTextCompletions: true,
includeCompletionsWithSnippetText: true,
}
});
@@ -0,0 +1,25 @@
/// <reference path="fourslash.ts" />
// @Filename: /mod.ts
//// export const foo = 0;
// @Filename: /noSemicolons.ts
//// import * as fs from "fs"
//// [|import f/**/|]
verify.completions({
marker: "",
exact: [{
name: "foo",
source: "./mod",
insertText: `import { foo$1 } from "./mod"`, // <-- no semicolon
isSnippet: true,
replacementSpan: test.ranges()[0],
sourceDisplay: "./mod",
}],
preferences: {
includeCompletionsForImportStatements: true,
includeInsertTextCompletions: true,
includeCompletionsWithSnippetText: true,
}
});
@@ -17,8 +17,8 @@ verify.completions({
name: "someModule",
source: "/someModule",
sourceDisplay: "./someModule",
text: "const someModule: 0",
kind: "const",
text: "(property) default: 1",
kind: "property",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions,
@@ -28,8 +28,8 @@ verify.completions({
name: "someModule",
source: "/someModule",
sourceDisplay: "./someModule",
text: "(property) default: 1",
kind: "property",
text: "const someModule: 0",
kind: "const",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions,