mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-05 08:11:30 -06:00
importFixes: Bundle module specifiers with import kinds, and replace ImportCodeActionMap with existing functionality (#20700)
This commit is contained in:
parent
73a86cb32d
commit
d6f52c3477
@ -15,14 +15,8 @@ namespace ts.codefix {
|
||||
getAllCodeActions: notImplemented,
|
||||
});
|
||||
|
||||
type ImportCodeActionKind = "CodeChange" | "InsertingIntoExistingImport" | "NewImport";
|
||||
// Map from module Id to an array of import declarations in that module.
|
||||
type ImportDeclarationMap = AnyImportSyntax[][];
|
||||
|
||||
interface ImportCodeAction extends CodeFixAction {
|
||||
kind: ImportCodeActionKind;
|
||||
moduleSpecifier?: string;
|
||||
}
|
||||
type ImportDeclarationMap = ExistingImportInfo[][];
|
||||
|
||||
interface SymbolContext extends textChanges.TextChangesContext {
|
||||
sourceFile: SourceFile;
|
||||
@ -38,127 +32,10 @@ namespace ts.codefix {
|
||||
cachedImportDeclarations?: ImportDeclarationMap;
|
||||
}
|
||||
|
||||
export interface ImportCodeFixOptions extends ImportCodeFixContext {
|
||||
kind: ImportKind;
|
||||
}
|
||||
|
||||
const enum ModuleSpecifierComparison {
|
||||
Better,
|
||||
Equal,
|
||||
Worse
|
||||
}
|
||||
|
||||
class ImportCodeActionMap {
|
||||
private symbolIdToActionMap: ImportCodeAction[][] = [];
|
||||
|
||||
addAction(symbolId: number, newAction: ImportCodeAction) {
|
||||
const actions = this.symbolIdToActionMap[symbolId];
|
||||
if (!actions) {
|
||||
this.symbolIdToActionMap[symbolId] = [newAction];
|
||||
return;
|
||||
}
|
||||
|
||||
if (newAction.kind === "CodeChange") {
|
||||
actions.push(newAction);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedNewImports: ImportCodeAction[] = [];
|
||||
for (const existingAction of this.symbolIdToActionMap[symbolId]) {
|
||||
if (existingAction.kind === "CodeChange") {
|
||||
// only import actions should compare
|
||||
updatedNewImports.push(existingAction);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (this.compareModuleSpecifiers(existingAction.moduleSpecifier, newAction.moduleSpecifier)) {
|
||||
case ModuleSpecifierComparison.Better:
|
||||
// the new one is not worth considering if it is a new import.
|
||||
// However if it is instead a insertion into existing import, the user might want to use
|
||||
// the module specifier even it is worse by our standards. So keep it.
|
||||
if (newAction.kind === "NewImport") {
|
||||
return;
|
||||
}
|
||||
// falls through
|
||||
case ModuleSpecifierComparison.Equal:
|
||||
// the current one is safe. But it is still possible that the new one is worse
|
||||
// than another existing one. For example, you may have new imports from "./foo/bar"
|
||||
// and "bar", when the new one is "bar/bar2" and the current one is "./foo/bar". The new
|
||||
// one and the current one are not comparable (one relative path and one absolute path),
|
||||
// but the new one is worse than the other one, so should not add to the list.
|
||||
updatedNewImports.push(existingAction);
|
||||
break;
|
||||
case ModuleSpecifierComparison.Worse:
|
||||
// the existing one is worse, remove from the list.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// if we reach here, it means the new one is better or equal to all of the existing ones.
|
||||
updatedNewImports.push(newAction);
|
||||
this.symbolIdToActionMap[symbolId] = updatedNewImports;
|
||||
}
|
||||
|
||||
addActions(symbolId: number, newActions: ImportCodeAction[]) {
|
||||
for (const newAction of newActions) {
|
||||
this.addAction(symbolId, newAction);
|
||||
}
|
||||
}
|
||||
|
||||
getAllActions() {
|
||||
let result: ImportCodeAction[] = [];
|
||||
for (const key in this.symbolIdToActionMap) {
|
||||
result = concatenate(result, this.symbolIdToActionMap[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private compareModuleSpecifiers(moduleSpecifier1: string, moduleSpecifier2: string): ModuleSpecifierComparison {
|
||||
if (moduleSpecifier1 === moduleSpecifier2) {
|
||||
return ModuleSpecifierComparison.Equal;
|
||||
}
|
||||
|
||||
// if moduleSpecifier1 (ms1) is a substring of ms2, then it is better
|
||||
if (moduleSpecifier2.indexOf(moduleSpecifier1) === 0) {
|
||||
return ModuleSpecifierComparison.Better;
|
||||
}
|
||||
|
||||
if (moduleSpecifier1.indexOf(moduleSpecifier2) === 0) {
|
||||
return ModuleSpecifierComparison.Worse;
|
||||
}
|
||||
|
||||
// if both are relative paths, and ms1 has fewer levels, then it is better
|
||||
if (isExternalModuleNameRelative(moduleSpecifier1) && isExternalModuleNameRelative(moduleSpecifier2)) {
|
||||
const regex = new RegExp(directorySeparator, "g");
|
||||
const moduleSpecifier1LevelCount = (moduleSpecifier1.match(regex) || []).length;
|
||||
const moduleSpecifier2LevelCount = (moduleSpecifier2.match(regex) || []).length;
|
||||
|
||||
return moduleSpecifier1LevelCount < moduleSpecifier2LevelCount
|
||||
? ModuleSpecifierComparison.Better
|
||||
: moduleSpecifier1LevelCount === moduleSpecifier2LevelCount
|
||||
? ModuleSpecifierComparison.Equal
|
||||
: ModuleSpecifierComparison.Worse;
|
||||
}
|
||||
|
||||
// the equal cases include when the two specifiers are not comparable.
|
||||
return ModuleSpecifierComparison.Equal;
|
||||
}
|
||||
}
|
||||
|
||||
function createCodeAction(
|
||||
description: DiagnosticMessage,
|
||||
diagnosticArgs: string[],
|
||||
changes: FileTextChanges[],
|
||||
kind: ImportCodeActionKind,
|
||||
moduleSpecifier: string | undefined,
|
||||
): ImportCodeAction {
|
||||
return {
|
||||
description: formatMessage.apply(undefined, [undefined, description].concat(<any[]>diagnosticArgs)),
|
||||
changes,
|
||||
// TODO: GH#20315
|
||||
fixId: undefined,
|
||||
kind,
|
||||
moduleSpecifier
|
||||
};
|
||||
function createCodeAction(descriptionDiagnostic: DiagnosticMessage, diagnosticArgs: string[], changes: FileTextChanges[]): CodeFixAction {
|
||||
const description = formatMessage.apply(undefined, [undefined, descriptionDiagnostic].concat(<any[]>diagnosticArgs));
|
||||
// TODO: GH#20315
|
||||
return { description, changes, fixId: undefined };
|
||||
}
|
||||
|
||||
function convertToImportCodeFixContext(context: CodeFixContext): ImportCodeFixContext {
|
||||
@ -181,42 +58,90 @@ namespace ts.codefix {
|
||||
};
|
||||
}
|
||||
|
||||
export const enum ImportKind {
|
||||
const enum ImportKind {
|
||||
Named,
|
||||
Default,
|
||||
Namespace,
|
||||
Equals
|
||||
}
|
||||
|
||||
export function getCodeActionForImport(moduleSymbols: Symbol | ReadonlyArray<Symbol>, context: ImportCodeFixOptions): ImportCodeAction[] {
|
||||
moduleSymbols = toArray(moduleSymbols);
|
||||
const declarations = flatMap(moduleSymbols, moduleSymbol =>
|
||||
getImportDeclarations(moduleSymbol, context.checker, context.sourceFile, context.cachedImportDeclarations));
|
||||
const actions: ImportCodeAction[] = [];
|
||||
if (context.symbolToken) {
|
||||
// It is possible that multiple import statements with the same specifier exist in the file.
|
||||
// e.g.
|
||||
//
|
||||
// import * as ns from "foo";
|
||||
// import { member1, member2 } from "foo";
|
||||
//
|
||||
// member3/**/ <-- cusor here
|
||||
//
|
||||
// in this case we should provie 2 actions:
|
||||
// 1. change "member3" to "ns.member3"
|
||||
// 2. add "member3" to the second import statement's import list
|
||||
// and it is up to the user to decide which one fits best.
|
||||
for (const declaration of declarations) {
|
||||
const namespace = getNamespaceImportName(declaration);
|
||||
if (namespace) {
|
||||
const moduleSymbol = context.checker.getAliasedSymbol(context.checker.getSymbolAtLocation(namespace));
|
||||
if (moduleSymbol && moduleSymbol.exports.has(escapeLeadingUnderscores(context.symbolName))) {
|
||||
actions.push(getCodeActionForUseExistingNamespaceImport(namespace.text, context, context.symbolToken));
|
||||
}
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/** Information needed to augment an existing import declaration. */
|
||||
interface ExistingImportInfo {
|
||||
readonly declaration: AnyImportSyntax;
|
||||
readonly importKind: ImportKind;
|
||||
}
|
||||
|
||||
/** Information needed to create a new import declaration. */
|
||||
interface NewImportInfo {
|
||||
readonly moduleSpecifier: string;
|
||||
readonly importKind: ImportKind;
|
||||
}
|
||||
|
||||
export function getImportCompletionAction(
|
||||
exportedSymbol: Symbol,
|
||||
moduleSymbol: Symbol,
|
||||
sourceFile: SourceFile,
|
||||
symbolName: string,
|
||||
host: LanguageServiceHost,
|
||||
program: ts.Program,
|
||||
checker: ts.TypeChecker,
|
||||
compilerOptions: ts.CompilerOptions,
|
||||
allSourceFiles: ReadonlyArray<ts.SourceFile>,
|
||||
formatContext: ts.formatting.FormatContext,
|
||||
getCanonicalFileName: GetCanonicalFileName,
|
||||
symbolToken: Identifier | undefined,
|
||||
): { readonly moduleSpecifier: string, readonly codeAction: CodeAction } {
|
||||
const exportInfos = getAllReExportingModules(exportedSymbol, checker, allSourceFiles);
|
||||
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol));
|
||||
// We sort the best codefixes first, so taking `first` is best for completions.
|
||||
const moduleSpecifier = first(getNewImportInfos(program, sourceFile, exportInfos, compilerOptions, getCanonicalFileName, host)).moduleSpecifier;
|
||||
const ctx: ImportCodeFixContext = { host, program, checker, compilerOptions, sourceFile, formatContext, symbolName, getCanonicalFileName, symbolToken };
|
||||
return { moduleSpecifier, codeAction: first(getCodeActionsForImport(exportInfos, ctx)) };
|
||||
}
|
||||
function getAllReExportingModules(exportedSymbol: Symbol, checker: TypeChecker, allSourceFiles: ReadonlyArray<SourceFile>): ReadonlyArray<SymbolExportInfo> {
|
||||
const result: SymbolExportInfo[] = [];
|
||||
forEachExternalModule(checker, allSourceFiles, moduleSymbol => {
|
||||
for (const exported of checker.getExportsOfModule(moduleSymbol)) {
|
||||
if (skipAlias(exported, checker) === exportedSymbol) {
|
||||
const isDefaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol) === exported;
|
||||
result.push({ moduleSymbol, importKind: isDefaultExport ? ImportKind.Default : ImportKind.Named });
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...actions, ...getCodeActionsForAddImport(moduleSymbols, context, declarations)];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function getCodeActionsForImport(exportInfos: ReadonlyArray<SymbolExportInfo>, context: ImportCodeFixContext): CodeFixAction[] {
|
||||
const existingImports = flatMap(exportInfos, info =>
|
||||
getImportDeclarations(info, context.checker, context.sourceFile, context.cachedImportDeclarations));
|
||||
// It is possible that multiple import statements with the same specifier exist in the file.
|
||||
// e.g.
|
||||
//
|
||||
// import * as ns from "foo";
|
||||
// import { member1, member2 } from "foo";
|
||||
//
|
||||
// member3/**/ <-- cusor here
|
||||
//
|
||||
// in this case we should provie 2 actions:
|
||||
// 1. change "member3" to "ns.member3"
|
||||
// 2. add "member3" to the second import statement's import list
|
||||
// and it is up to the user to decide which one fits best.
|
||||
const useExistingImportActions = !context.symbolToken ? emptyArray : mapDefined(existingImports, ({ declaration }) => {
|
||||
const namespace = getNamespaceImportName(declaration);
|
||||
if (namespace) {
|
||||
const moduleSymbol = context.checker.getAliasedSymbol(context.checker.getSymbolAtLocation(namespace));
|
||||
if (moduleSymbol && moduleSymbol.exports.has(escapeLeadingUnderscores(context.symbolName))) {
|
||||
return getCodeActionForUseExistingNamespaceImport(namespace.text, context, context.symbolToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
return [...useExistingImportActions, ...getCodeActionsForAddImport(exportInfos, context, existingImports)];
|
||||
}
|
||||
|
||||
function getNamespaceImportName(declaration: AnyImportSyntax): Identifier | undefined {
|
||||
@ -230,12 +155,14 @@ namespace ts.codefix {
|
||||
}
|
||||
|
||||
// TODO(anhans): This doesn't seem important to cache... just use an iterator instead of creating a new array?
|
||||
function getImportDeclarations(moduleSymbol: Symbol, checker: TypeChecker, { imports }: SourceFile, cachedImportDeclarations: ImportDeclarationMap = []): ReadonlyArray<AnyImportSyntax> {
|
||||
function getImportDeclarations({ moduleSymbol, importKind }: SymbolExportInfo, checker: TypeChecker, { imports }: SourceFile, cachedImportDeclarations: ImportDeclarationMap = []): ReadonlyArray<ExistingImportInfo> {
|
||||
const moduleSymbolId = getUniqueSymbolId(moduleSymbol, checker);
|
||||
let cached = cachedImportDeclarations[moduleSymbolId];
|
||||
if (!cached) {
|
||||
cached = cachedImportDeclarations[moduleSymbolId] = mapDefined(imports, importModuleSpecifier =>
|
||||
checker.getSymbolAtLocation(importModuleSpecifier) === moduleSymbol ? getImportDeclaration(importModuleSpecifier) : undefined);
|
||||
cached = cachedImportDeclarations[moduleSymbolId] = mapDefined<StringLiteral, ExistingImportInfo>(imports, importModuleSpecifier => {
|
||||
const declaration = checker.getSymbolAtLocation(importModuleSpecifier) === moduleSymbol ? getImportDeclaration(importModuleSpecifier) : undefined;
|
||||
return declaration && { declaration, importKind };
|
||||
});
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
@ -255,17 +182,17 @@ namespace ts.codefix {
|
||||
}
|
||||
}
|
||||
|
||||
function getCodeActionForNewImport(context: SymbolContext & { kind: ImportKind }, moduleSpecifier: string): ImportCodeAction {
|
||||
const { kind, sourceFile, symbolName } = context;
|
||||
function getCodeActionForNewImport(context: SymbolContext, { moduleSpecifier, importKind }: NewImportInfo): CodeFixAction {
|
||||
const { sourceFile, symbolName } = context;
|
||||
const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax);
|
||||
|
||||
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier);
|
||||
const quotedModuleSpecifier = createStringLiteralWithQuoteStyle(sourceFile, moduleSpecifierWithoutQuotes);
|
||||
const importDecl = kind !== ImportKind.Equals
|
||||
const importDecl = importKind !== ImportKind.Equals
|
||||
? createImportDeclaration(
|
||||
/*decorators*/ undefined,
|
||||
/*modifiers*/ undefined,
|
||||
createImportClauseOfKind(kind, symbolName),
|
||||
createImportClauseOfKind(importKind, symbolName),
|
||||
quotedModuleSpecifier)
|
||||
: createImportEqualsDeclaration(
|
||||
/*decorators*/ undefined,
|
||||
@ -285,13 +212,7 @@ namespace ts.codefix {
|
||||
// if this file doesn't have any import statements, insert an import statement and then insert a new line
|
||||
// between the only import statement and user code. Otherwise just insert the statement because chances
|
||||
// are there are already a new line seperating code and import statements.
|
||||
return createCodeAction(
|
||||
Diagnostics.Import_0_from_module_1,
|
||||
[symbolName, moduleSpecifierWithoutQuotes],
|
||||
changes,
|
||||
"NewImport",
|
||||
moduleSpecifierWithoutQuotes,
|
||||
);
|
||||
return createCodeAction(Diagnostics.Import_0_from_module_1, [symbolName, moduleSpecifierWithoutQuotes], changes);
|
||||
}
|
||||
|
||||
function createStringLiteralWithQuoteStyle(sourceFile: SourceFile, text: string): StringLiteral {
|
||||
@ -319,18 +240,18 @@ namespace ts.codefix {
|
||||
}
|
||||
}
|
||||
|
||||
export function getModuleSpecifiersForNewImport(
|
||||
function getNewImportInfos(
|
||||
program: Program,
|
||||
sourceFile: SourceFile,
|
||||
moduleSymbols: ReadonlyArray<Symbol>,
|
||||
moduleSymbols: ReadonlyArray<SymbolExportInfo>,
|
||||
options: CompilerOptions,
|
||||
getCanonicalFileName: (file: string) => string,
|
||||
host: LanguageServiceHost,
|
||||
): string[] {
|
||||
): ReadonlyArray<NewImportInfo> {
|
||||
const { baseUrl, paths, rootDirs } = options;
|
||||
const addJsExtension = usesJsExtensionOnImports(sourceFile);
|
||||
const choicesForEachExportingModule = flatMap(moduleSymbols, moduleSymbol =>
|
||||
getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile()).map(moduleFileName => {
|
||||
const choicesForEachExportingModule = flatMap<SymbolExportInfo, NewImportInfo[]>(moduleSymbols, ({ moduleSymbol, importKind }) => {
|
||||
const modulePathsGroups = getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile()).map(moduleFileName => {
|
||||
const sourceDirectory = getDirectoryPath(sourceFile.fileName);
|
||||
const global = tryGetModuleNameFromAmbientModule(moduleSymbol)
|
||||
|| tryGetModuleNameFromTypeRoots(options, host, getCanonicalFileName, moduleFileName, addJsExtension)
|
||||
@ -392,9 +313,11 @@ namespace ts.codefix {
|
||||
const pathFromSourceToBaseUrl = getRelativePath(baseUrl, sourceDirectory, getCanonicalFileName);
|
||||
const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl);
|
||||
return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath];
|
||||
}));
|
||||
// Only return results for the re-export with the shortest possible path (and also give the other path even if that's long.)
|
||||
return best(arrayIterator(choicesForEachExportingModule), (a, b) => a[0].length < b[0].length);
|
||||
});
|
||||
return modulePathsGroups.map(group => group.map(moduleSpecifier => ({ moduleSpecifier, importKind })));
|
||||
});
|
||||
// Sort to keep the shortest paths first, but keep [relativePath, importRelativeToBaseUrl] groups together
|
||||
return flatten<NewImportInfo>(choicesForEachExportingModule.sort((a, b) => first(a).moduleSpecifier.length - first(b).moduleSpecifier.length));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -631,21 +554,16 @@ namespace ts.codefix {
|
||||
}
|
||||
|
||||
function getCodeActionsForAddImport(
|
||||
moduleSymbols: ReadonlyArray<Symbol>,
|
||||
ctx: ImportCodeFixOptions,
|
||||
declarations: ReadonlyArray<AnyImportSyntax>
|
||||
): ImportCodeAction[] {
|
||||
const fromExistingImport = firstDefined(declarations, declaration => {
|
||||
exportInfos: ReadonlyArray<SymbolExportInfo>,
|
||||
ctx: ImportCodeFixContext,
|
||||
existingImports: ReadonlyArray<ExistingImportInfo>,
|
||||
): CodeFixAction[] {
|
||||
const fromExistingImport = firstDefined(existingImports, ({ declaration, importKind }) => {
|
||||
if (declaration.kind === SyntaxKind.ImportDeclaration && declaration.importClause) {
|
||||
const changes = tryUpdateExistingImport(ctx, isImportClause(declaration.importClause) && declaration.importClause || undefined);
|
||||
const changes = tryUpdateExistingImport(ctx, isImportClause(declaration.importClause) && declaration.importClause || undefined, importKind);
|
||||
if (changes) {
|
||||
const moduleSpecifierWithoutQuotes = stripQuotes(declaration.moduleSpecifier.getText());
|
||||
return createCodeAction(
|
||||
Diagnostics.Add_0_to_existing_import_declaration_from_1,
|
||||
[ctx.symbolName, moduleSpecifierWithoutQuotes],
|
||||
changes,
|
||||
"InsertingIntoExistingImport",
|
||||
moduleSpecifierWithoutQuotes);
|
||||
return createCodeAction(Diagnostics.Add_0_to_existing_import_declaration_from_1, [ctx.symbolName, moduleSpecifierWithoutQuotes], changes);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -653,25 +571,27 @@ namespace ts.codefix {
|
||||
return [fromExistingImport];
|
||||
}
|
||||
|
||||
const existingDeclaration = firstDefined(declarations, moduleSpecifierFromAnyImport);
|
||||
const moduleSpecifiers = existingDeclaration ? [existingDeclaration] : getModuleSpecifiersForNewImport(ctx.program, ctx.sourceFile, moduleSymbols, ctx.compilerOptions, ctx.getCanonicalFileName, ctx.host);
|
||||
return moduleSpecifiers.map(spec => getCodeActionForNewImport(ctx, spec));
|
||||
const existingDeclaration = firstDefined(existingImports, newImportInfoFromExistingSpecifier);
|
||||
const newImportInfos = existingDeclaration
|
||||
? [existingDeclaration]
|
||||
: getNewImportInfos(ctx.program, ctx.sourceFile, exportInfos, ctx.compilerOptions, ctx.getCanonicalFileName, ctx.host);
|
||||
return newImportInfos.map(info => getCodeActionForNewImport(ctx, info));
|
||||
}
|
||||
|
||||
function moduleSpecifierFromAnyImport(node: AnyImportSyntax): string | undefined {
|
||||
const expression = node.kind === SyntaxKind.ImportDeclaration
|
||||
? node.moduleSpecifier
|
||||
: node.moduleReference.kind === SyntaxKind.ExternalModuleReference
|
||||
? node.moduleReference.expression
|
||||
function newImportInfoFromExistingSpecifier({ declaration, importKind }: ExistingImportInfo): NewImportInfo | undefined {
|
||||
const expression = declaration.kind === SyntaxKind.ImportDeclaration
|
||||
? declaration.moduleSpecifier
|
||||
: declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference
|
||||
? declaration.moduleReference.expression
|
||||
: undefined;
|
||||
return expression && isStringLiteral(expression) ? expression.text : undefined;
|
||||
return expression && isStringLiteral(expression) ? { moduleSpecifier: expression.text, importKind } : undefined;
|
||||
}
|
||||
|
||||
function tryUpdateExistingImport(context: SymbolContext & { kind: ImportKind }, importClause: ImportClause | ImportEqualsDeclaration): FileTextChanges[] | undefined {
|
||||
const { symbolName, sourceFile, kind } = context;
|
||||
function tryUpdateExistingImport(context: SymbolContext, importClause: ImportClause | ImportEqualsDeclaration, importKind: ImportKind): FileTextChanges[] | undefined {
|
||||
const { symbolName, sourceFile } = context;
|
||||
const { name } = importClause;
|
||||
const { namedBindings } = importClause.kind !== SyntaxKind.ImportEqualsDeclaration && importClause;
|
||||
switch (kind) {
|
||||
switch (importKind) {
|
||||
case ImportKind.Default:
|
||||
return name ? undefined : ChangeTracker.with(context, t =>
|
||||
t.replaceNode(sourceFile, importClause, createImportClause(createIdentifier(symbolName), namedBindings)));
|
||||
@ -700,11 +620,11 @@ namespace ts.codefix {
|
||||
return undefined;
|
||||
|
||||
default:
|
||||
Debug.assertNever(kind);
|
||||
Debug.assertNever(importKind);
|
||||
}
|
||||
}
|
||||
|
||||
function getCodeActionForUseExistingNamespaceImport(namespacePrefix: string, context: SymbolContext, symbolToken: Identifier): ImportCodeAction {
|
||||
function getCodeActionForUseExistingNamespaceImport(namespacePrefix: string, context: SymbolContext, symbolToken: Identifier): CodeFixAction {
|
||||
const { symbolName, sourceFile } = context;
|
||||
|
||||
/**
|
||||
@ -717,20 +637,19 @@ namespace ts.codefix {
|
||||
* namespace instead of altering the import declaration. For example, "foo" would
|
||||
* become "ns.foo"
|
||||
*/
|
||||
// Prefix the node instead of it replacing it, because this may be used for import completions and we don't want the text changes to overlap with the identifier being completed.
|
||||
const changes = ChangeTracker.with(context, tracker =>
|
||||
tracker.changeIdentifierToPropertyAccess(sourceFile, namespacePrefix, symbolToken));
|
||||
return createCodeAction(Diagnostics.Change_0_to_1, [symbolName, `${namespacePrefix}.${symbolName}`], changes, "CodeChange", /*moduleSpecifier*/ undefined);
|
||||
return createCodeAction(Diagnostics.Change_0_to_1, [symbolName, `${namespacePrefix}.${symbolName}`], changes);
|
||||
}
|
||||
|
||||
function getImportCodeActions(context: CodeFixContext): ImportCodeAction[] {
|
||||
function getImportCodeActions(context: CodeFixContext): CodeAction[] {
|
||||
const importFixContext = convertToImportCodeFixContext(context);
|
||||
return context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code
|
||||
? getActionsForUMDImport(importFixContext)
|
||||
: getActionsForNonUMDImport(importFixContext, context.program.getSourceFiles(), context.cancellationToken);
|
||||
}
|
||||
|
||||
function getActionsForUMDImport(context: ImportCodeFixContext): ImportCodeAction[] {
|
||||
function getActionsForUMDImport(context: ImportCodeFixContext): CodeAction[] {
|
||||
const { checker, symbolToken, compilerOptions } = context;
|
||||
const umdSymbol = checker.getSymbolAtLocation(symbolToken);
|
||||
let symbol: ts.Symbol;
|
||||
@ -748,7 +667,7 @@ namespace ts.codefix {
|
||||
throw Debug.fail("Either the symbol or the JSX namespace should be a UMD global if we got here");
|
||||
}
|
||||
|
||||
return getCodeActionForImport(symbol, { ...context, symbolName, kind: getUmdImportKind(compilerOptions) });
|
||||
return getCodeActionsForImport([{ moduleSymbol: symbol, importKind: getUmdImportKind(compilerOptions) }], { ...context, symbolName });
|
||||
}
|
||||
function getUmdImportKind(compilerOptions: CompilerOptions) {
|
||||
// Import a synthetic `default` if enabled.
|
||||
@ -774,15 +693,21 @@ namespace ts.codefix {
|
||||
}
|
||||
}
|
||||
|
||||
function getActionsForNonUMDImport(context: ImportCodeFixContext, allSourceFiles: ReadonlyArray<SourceFile>, cancellationToken: CancellationToken): ImportCodeAction[] {
|
||||
function getActionsForNonUMDImport(context: ImportCodeFixContext, allSourceFiles: ReadonlyArray<SourceFile>, cancellationToken: CancellationToken): CodeAction[] {
|
||||
const { sourceFile, checker, symbolName, symbolToken } = context;
|
||||
// "default" is a keyword and not a legal identifier for the import, so we don't expect it here
|
||||
Debug.assert(symbolName !== "default");
|
||||
const symbolIdActionMap = new ImportCodeActionMap();
|
||||
const currentTokenMeaning = getMeaningFromLocation(symbolToken);
|
||||
|
||||
// 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): void {
|
||||
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { moduleSymbol, importKind });
|
||||
}
|
||||
forEachExternalModuleToImportFrom(checker, sourceFile, allSourceFiles, moduleSymbol => {
|
||||
cancellationToken.throwIfCancellationRequested();
|
||||
|
||||
// check the default export
|
||||
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
|
||||
if (defaultExport) {
|
||||
@ -792,17 +717,14 @@ namespace ts.codefix {
|
||||
getEscapedNameForExportDefault(defaultExport) === symbolName ||
|
||||
moduleSymbolToValidIdentifier(moduleSymbol, context.compilerOptions.target) === symbolName
|
||||
) && checkSymbolHasMeaning(localSymbol || defaultExport, currentTokenMeaning)) {
|
||||
// check if this symbol is already used
|
||||
const symbolId = getUniqueSymbolId(localSymbol || defaultExport, checker);
|
||||
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, { ...context, kind: ImportKind.Default }));
|
||||
addSymbol(moduleSymbol, localSymbol || defaultExport, ImportKind.Default);
|
||||
}
|
||||
}
|
||||
|
||||
// check exports with the same name
|
||||
const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol);
|
||||
if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) {
|
||||
const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName, checker);
|
||||
symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, { ...context, kind: ImportKind.Named }));
|
||||
addSymbol(moduleSymbol, exportSymbolWithIdenticalName, ImportKind.Named);
|
||||
}
|
||||
|
||||
function getEscapedNameForExportDefault(symbol: Symbol): __String | undefined {
|
||||
@ -822,7 +744,7 @@ namespace ts.codefix {
|
||||
}
|
||||
});
|
||||
|
||||
return symbolIdActionMap.getAllActions();
|
||||
return arrayFrom(flatMapIterator(originalSymbolToExportInfos.values(), exportInfos => getCodeActionsForImport(exportInfos, context)));
|
||||
}
|
||||
|
||||
function checkSymbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean {
|
||||
@ -837,7 +759,7 @@ namespace ts.codefix {
|
||||
});
|
||||
}
|
||||
|
||||
export function forEachExternalModule(checker: TypeChecker, allSourceFiles: ReadonlyArray<SourceFile>, cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
|
||||
function forEachExternalModule(checker: TypeChecker, allSourceFiles: ReadonlyArray<SourceFile>, cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
|
||||
for (const ambient of checker.getAmbientModules()) {
|
||||
cb(ambient, /*sourceFile*/ undefined);
|
||||
}
|
||||
|
||||
@ -633,37 +633,22 @@ namespace ts.Completions {
|
||||
getCanonicalFileName: GetCanonicalFileName,
|
||||
allSourceFiles: ReadonlyArray<SourceFile>
|
||||
): CodeActionsAndSourceDisplay {
|
||||
const { moduleSymbol, isDefaultExport } = symbolOriginInfo;
|
||||
const { moduleSymbol } = symbolOriginInfo;
|
||||
const exportedSymbol = skipAlias(symbol.exportSymbol || symbol, checker);
|
||||
const moduleSymbols = getAllReExportingModules(exportedSymbol, checker, allSourceFiles);
|
||||
Debug.assert(contains(moduleSymbols, moduleSymbol));
|
||||
|
||||
const sourceDisplay = [textPart(first(codefix.getModuleSpecifiersForNewImport(program, sourceFile, moduleSymbols, compilerOptions, getCanonicalFileName, host)))];
|
||||
const codeActions = codefix.getCodeActionForImport(moduleSymbols, {
|
||||
const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction(
|
||||
exportedSymbol,
|
||||
moduleSymbol,
|
||||
sourceFile,
|
||||
getSymbolName(symbol, symbolOriginInfo, compilerOptions.target),
|
||||
host,
|
||||
program,
|
||||
checker,
|
||||
compilerOptions,
|
||||
sourceFile,
|
||||
allSourceFiles,
|
||||
formatContext,
|
||||
symbolName: getSymbolName(symbol, symbolOriginInfo, compilerOptions.target),
|
||||
getCanonicalFileName,
|
||||
symbolToken: tryCast(previousToken, isIdentifier),
|
||||
kind: isDefaultExport ? codefix.ImportKind.Default : codefix.ImportKind.Named,
|
||||
}).slice(0, 1); // Only take the first code action
|
||||
return { sourceDisplay, codeActions };
|
||||
}
|
||||
|
||||
function getAllReExportingModules(exportedSymbol: Symbol, checker: TypeChecker, allSourceFiles: ReadonlyArray<SourceFile>): ReadonlyArray<Symbol> {
|
||||
const result: Symbol[] = [];
|
||||
codefix.forEachExternalModule(checker, allSourceFiles, module => {
|
||||
for (const exported of checker.getExportsOfModule(module)) {
|
||||
if (skipAlias(exported, checker) === exportedSymbol) {
|
||||
result.push(module);
|
||||
}
|
||||
}
|
||||
});
|
||||
return result;
|
||||
tryCast(previousToken, isIdentifier));
|
||||
return { sourceDisplay: [textPart(moduleSpecifier)], codeActions: [codeAction] };
|
||||
}
|
||||
|
||||
export function getCompletionEntrySymbol(
|
||||
@ -1264,6 +1249,9 @@ namespace ts.Completions {
|
||||
codefix.forEachExternalModuleToImportFrom(typeChecker, sourceFile, allSourceFiles, moduleSymbol => {
|
||||
for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) {
|
||||
// Don't add a completion for a re-export, only for the original.
|
||||
// The actual import fix might end up coming from a re-export -- we don't compute that until getting completion details.
|
||||
// This is just to avoid adding duplicate completion entries.
|
||||
//
|
||||
// If `symbol.parent !== ...`, this comes from an `export * from "foo"` re-export. Those don't create new symbols.
|
||||
// If `some(...)`, this comes from an `export { foo } from "foo"` re-export, which creates a new symbol (thus isn't caught by the first check).
|
||||
if (typeChecker.getMergedSymbol(symbol.parent) !== typeChecker.resolveExternalModuleSymbol(moduleSymbol)
|
||||
|
||||
24
tests/cases/fourslash/completionsImport_reExportDefault.ts
Normal file
24
tests/cases/fourslash/completionsImport_reExportDefault.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/// <reference path="fourslash.ts" />
|
||||
|
||||
// @Filename: /a/b/impl.ts
|
||||
////export default function foo() {}
|
||||
|
||||
// @Filename: /a/index.ts
|
||||
////export { default as foo } from "./b/impl";
|
||||
|
||||
// @Filename: /use.ts
|
||||
////fo/**/
|
||||
|
||||
goTo.marker("");
|
||||
verify.completionListContains({ name: "foo", source: "/a/b/impl" }, "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true, {
|
||||
includeExternalModuleExports: true,
|
||||
sourceDisplay: "./a",
|
||||
});
|
||||
verify.applyCodeActionFromCompletion("", {
|
||||
name: "foo",
|
||||
source: "/a/b/impl",
|
||||
description: `Import 'foo' from module "./a"`,
|
||||
newFileContent: `import { foo } from "./a";
|
||||
|
||||
fo`,
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user