Minor cleanups in importFixes (#23995)

This commit is contained in:
Andy 2018-05-24 07:46:39 -07:00 committed by GitHub
parent ac0657a5d7
commit ee8337d8e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 53 additions and 64 deletions

View file

@ -965,7 +965,8 @@ namespace ts {
export function append<T>(to: T[], value: T | undefined): T[];
export function append<T>(to: T[] | undefined, value: T): T[];
export function append<T>(to: T[] | undefined, value: T | undefined): T[] | undefined;
export function append<T>(to: T[] | undefined, value: T | undefined): T[] | undefined {
export function append<T>(to: Push<T>, value: T | undefined): void;
export function append<T>(to: T[], value: T | undefined): T[] | undefined {
if (value === undefined) return to;
if (to === undefined) return [value];
to.push(value);

View file

@ -10,15 +10,14 @@ namespace ts.codefix {
Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code,
Diagnostics._0_only_refers_to_a_type_but_is_being_used_as_a_value_here.code,
],
getCodeActions: getImportCodeActions,
getCodeActions: context => context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code
? getActionsForUMDImport(context)
: getActionsForNonUMDImport(context),
// TODO: GH#20315
fixIds: [],
getAllCodeActions: notImplemented,
});
// Map from module Id to an array of import declarations in that module.
type ImportDeclarationMap = ExistingImportInfo[][];
interface SymbolContext extends textChanges.TextChangesContext {
sourceFile: SourceFile;
symbolName: string;
@ -30,7 +29,6 @@ namespace ts.codefix {
checker: TypeChecker;
compilerOptions: CompilerOptions;
getCanonicalFileName: GetCanonicalFileName;
cachedImportDeclarations?: ImportDeclarationMap;
preferences: UserPreferences;
}
@ -50,7 +48,6 @@ namespace ts.codefix {
program,
checker,
compilerOptions: program.getCompilerOptions(),
cachedImportDeclarations: [],
getCanonicalFileName: createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(context.host)),
symbolName,
symbolToken,
@ -130,8 +127,19 @@ namespace ts.codefix {
}
function getCodeActionsForImport_separateExistingAndNew(exportInfos: ReadonlyArray<SymbolExportInfo>, context: ImportCodeFixContext, useExisting: Push<CodeFixAction>, addNew: Push<CodeFixAction>): void {
const existingImports = flatMap(exportInfos, info =>
getImportDeclarations(info, context.checker, context.sourceFile, context.cachedImportDeclarations));
const existingImports = flatMap(exportInfos, info => getExistingImportDeclarations(info, context.checker, context.sourceFile));
append(useExisting, tryUseExistingNamespaceImport(existingImports, context, context.symbolToken, context.checker));
const addToExisting = tryAddToExistingImport(existingImports, context);
if (addToExisting) {
useExisting.push(addToExisting);
}
else { // Don't bother providing an action to add a new import if we can add to an existing one.
getCodeActionsForAddImport(exportInfos, context, existingImports, addNew);
}
}
function tryUseExistingNamespaceImport(existingImports: ReadonlyArray<ExistingImportInfo>, context: SymbolContext, symbolToken: Node | undefined, checker: TypeChecker): CodeFixAction | undefined {
// It is possible that multiple import statements with the same specifier exist in the file.
// e.g.
//
@ -144,18 +152,26 @@ namespace ts.codefix {
// 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.
if (context.symbolToken && isIdentifier(context.symbolToken)) {
for (const { declaration } of existingImports) {
const namespace = getNamespaceImportName(declaration);
if (namespace) {
const moduleSymbol = context.checker.getAliasedSymbol(context.checker.getSymbolAtLocation(namespace)!);
if (moduleSymbol && moduleSymbol.exports!.has(escapeLeadingUnderscores(context.symbolName))) {
useExisting.push(getCodeActionForUseExistingNamespaceImport(namespace.text, context, context.symbolToken));
}
return !symbolToken || !isIdentifier(symbolToken) ? undefined : firstDefined(existingImports, ({ declaration }) => {
const namespace = getNamespaceImportName(declaration);
if (namespace) {
const moduleSymbol = namespace && checker.getAliasedSymbol(checker.getSymbolAtLocation(namespace)!);
if (moduleSymbol && moduleSymbol.exports!.has(escapeLeadingUnderscores(context.symbolName))) {
return getCodeActionForUseExistingNamespaceImport(namespace.text, context, symbolToken);
}
}
}
getCodeActionsForAddImport(exportInfos, context, existingImports, useExisting, addNew);
});
}
function tryAddToExistingImport(existingImports: ReadonlyArray<ExistingImportInfo>, context: SymbolContext): CodeFixAction | undefined {
return firstDefined(existingImports, ({ declaration, importKind }) => {
if (declaration.kind === SyntaxKind.ImportDeclaration && declaration.importClause) {
const changes = tryUpdateExistingImport(context, declaration.importClause, importKind);
if (changes) {
const moduleSpecifierWithoutQuotes = stripQuotes(declaration.moduleSpecifier.getText());
return createCodeAction(Diagnostics.Add_0_to_existing_import_declaration_from_1, [context.symbolName, moduleSpecifierWithoutQuotes], changes);
}
}
});
}
function getNamespaceImportName(declaration: AnyImportSyntax): Identifier | undefined {
@ -168,18 +184,12 @@ 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, 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<StringLiteralLike, ExistingImportInfo>(imports, moduleSpecifier => {
const i = importFromModuleSpecifier(moduleSpecifier);
return (i.kind === SyntaxKind.ImportDeclaration || i.kind === SyntaxKind.ImportEqualsDeclaration)
&& checker.getSymbolAtLocation(moduleSpecifier) === moduleSymbol ? { declaration: i, importKind } : undefined;
});
}
return cached;
function getExistingImportDeclarations({ moduleSymbol, importKind }: SymbolExportInfo, checker: TypeChecker, { imports }: SourceFile): ReadonlyArray<ExistingImportInfo> {
return mapDefined<StringLiteralLike, ExistingImportInfo>(imports, moduleSpecifier => {
const i = importFromModuleSpecifier(moduleSpecifier);
return (i.kind === SyntaxKind.ImportDeclaration || i.kind === SyntaxKind.ImportEqualsDeclaration)
&& checker.getSymbolAtLocation(moduleSpecifier) === moduleSymbol ? { declaration: i, importKind } : undefined;
});
}
function getCodeActionForNewImport(context: SymbolContext & { preferences: UserPreferences }, { moduleSpecifier, importKind }: NewImportInfo): CodeFixAction {
@ -258,23 +268,8 @@ namespace ts.codefix {
exportInfos: ReadonlyArray<SymbolExportInfo>,
ctx: ImportCodeFixContext,
existingImports: ReadonlyArray<ExistingImportInfo>,
useExisting: Push<CodeFixAction>,
addNew: Push<CodeFixAction>,
): void {
const fromExistingImport = firstDefined(existingImports, ({ declaration, importKind }) => {
if (declaration.kind === SyntaxKind.ImportDeclaration && declaration.importClause) {
const changes = tryUpdateExistingImport(ctx, (isImportClause(declaration.importClause) && declaration.importClause || undefined)!, importKind); // TODO: GH#18217
if (changes) {
const moduleSpecifierWithoutQuotes = stripQuotes(declaration.moduleSpecifier.getText());
return createCodeAction(Diagnostics.Add_0_to_existing_import_declaration_from_1, [ctx.symbolName, moduleSpecifierWithoutQuotes], changes);
}
}
});
if (fromExistingImport) {
useExisting.push(fromExistingImport);
return;
}
const existingDeclaration = firstDefined(existingImports, newImportInfoFromExistingSpecifier);
const newImportInfos = existingDeclaration
? [existingDeclaration]
@ -348,12 +343,6 @@ namespace ts.codefix {
return createCodeAction(Diagnostics.Change_0_to_1, [symbolName, `${namespacePrefix}.${symbolName}`], changes);
}
function getImportCodeActions(context: CodeFixContext): CodeFixAction[] | undefined {
return context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code
? getActionsForUMDImport(context)
: getActionsForNonUMDImport(context);
}
function getActionsForUMDImport(context: CodeFixContext): CodeFixAction[] | undefined {
const token = getTokenAtPosition(context.sourceFile, context.span.start, /*includeJsDocComment*/ false);
const checker = context.program.getTypeChecker();
@ -422,11 +411,18 @@ namespace ts.codefix {
? checker.getJsxNamespace()
: isIdentifier(symbolToken) ? symbolToken.text : undefined;
if (!symbolName) return undefined;
// "default" is a keyword and not a legal identifier for the import, so we don't expect it here
Debug.assert(symbolName !== "default");
const currentTokenMeaning = getMeaningFromLocation(symbolToken);
const addToExistingDeclaration: CodeFixAction[] = [];
const addNewDeclaration: CodeFixAction[] = [];
getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, checker, program).forEach(exportInfos => {
getCodeActionsForImport_separateExistingAndNew(exportInfos, convertToImportCodeFixContext(context, symbolToken, symbolName), addToExistingDeclaration, addNewDeclaration);
});
return [...addToExistingDeclaration, ...addNewDeclaration];
}
function getExportInfos(symbolName: string, currentTokenMeaning: SemanticMeaning, cancellationToken: CancellationToken, sourceFile: SourceFile, checker: TypeChecker, program: Program): ReadonlyMap<ReadonlyArray<SymbolExportInfo>> {
// 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>();
@ -464,20 +460,12 @@ namespace ts.codefix {
}
else if (isExportSpecifier(declaration)) {
Debug.assert(declaration.name.escapedText === InternalSymbolName.Default);
if (declaration.propertyName) {
return declaration.propertyName.escapedText;
}
return declaration.propertyName && declaration.propertyName.escapedText;
}
});
}
});
const addToExistingDeclaration: CodeFixAction[] = [];
const addNewDeclaration: CodeFixAction[] = [];
originalSymbolToExportInfos.forEach(exportInfos => {
getCodeActionsForImport_separateExistingAndNew(exportInfos, convertToImportCodeFixContext(context, symbolToken, symbolName), addToExistingDeclaration, addNewDeclaration);
});
return [...addToExistingDeclaration, ...addNewDeclaration];
return originalSymbolToExportInfos;
}
function checkSymbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean {