Convert use-default-import refactor to a codefix (#22334)

This commit is contained in:
Andy
2018-03-05 12:43:01 -08:00
committed by GitHub
parent 3bcfed61b6
commit 2ac2291b84
14 changed files with 137 additions and 142 deletions

View File

@@ -3826,6 +3826,10 @@
"category": "Suggestion",
"code": 80002
},
"Import may be converted to a default import.": {
"category": "Suggestion",
"code": 80003
},
"Add missing 'super()' call": {
"category": "Message",

View File

@@ -96,7 +96,7 @@ namespace ts {
}
function eachDiagnostic({ program, sourceFile }: CodeFixAllContext, errorCodes: number[], cb: (diag: Diagnostic) => void): void {
for (const diag of program.getSemanticDiagnostics(sourceFile)) {
for (const diag of program.getSemanticDiagnostics(sourceFile).concat(computeSuggestionDiagnostics(sourceFile, program))) {
if (contains(errorCodes, diag.code)) {
cb(diag);
}

View File

@@ -495,9 +495,13 @@ namespace ts.codefix {
: makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier);
}
function makeImport(name: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier>, moduleSpecifier: string): ImportDeclaration {
function makeImport(name: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: string): ImportDeclaration {
return makeImportDeclaration(name, namedImports, createLiteral(moduleSpecifier));
}
export function makeImportDeclaration(name: Identifier, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: Expression) {
const importClause = (name || namedImports) && createImportClause(name, namedImports && createNamedImports(namedImports));
return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, createLiteral(moduleSpecifier));
return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, moduleSpecifier);
}
function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier {

View File

@@ -26,12 +26,7 @@ namespace ts.codefix {
const variations: CodeAction[] = [];
// import Bluebird from "bluebird";
variations.push(createAction(context, sourceFile, node, createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createImportClause(namespace.name, /*namedBindings*/ undefined),
node.moduleSpecifier
)));
variations.push(createAction(context, sourceFile, node, makeImportDeclaration(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier)));
if (getEmitModuleKind(opts) === ModuleKind.CommonJS) {
// import Bluebird = require("bluebird");

View File

@@ -20,3 +20,4 @@
/// <reference path='inferFromUsage.ts' />
/// <reference path="fixInvalidImportSyntax.ts" />
/// <reference path="fixStrictClassInitialization.ts" />
/// <reference path="useDefaultImport.ts" />

View File

@@ -0,0 +1,43 @@
/* @internal */
namespace ts.codefix {
const fixId = "useDefaultImport";
const errorCodes = [Diagnostics.Import_may_be_converted_to_a_default_import.code];
registerCodeFix({
errorCodes,
getCodeActions(context) {
const { sourceFile, span: { start } } = context;
const info = getInfo(sourceFile, start);
if (!info) return undefined;
const description = getLocaleSpecificMessage(Diagnostics.Convert_to_default_import);
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, info));
return [{ description, changes, fixId }];
},
fixIds: [fixId],
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
const info = getInfo(diag.file!, diag.start!);
if (info) doChange(changes, diag.file!, info);
}),
});
interface Info {
readonly importNode: AnyImportSyntax;
readonly name: Identifier;
readonly moduleSpecifier: Expression;
}
function getInfo(sourceFile: SourceFile, pos: number): Info | undefined {
const name = getTokenAtPosition(sourceFile, pos, /*includeJsDocComment*/ false);
if (!isIdentifier(name)) return undefined; // bad input
const { parent } = name;
if (isImportEqualsDeclaration(parent) && isExternalModuleReference(parent.moduleReference)) {
return { importNode: parent, name, moduleSpecifier: parent.moduleReference.expression };
}
else if (isNamespaceImport(parent)) {
const importNode = parent.parent.parent;
return { importNode, name, moduleSpecifier: importNode.moduleSpecifier };
}
}
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info): void {
changes.replaceNode(sourceFile, info.importNode, makeImportDeclaration(info.name, /*namedImports*/ undefined, info.moduleSpecifier));
}
}

View File

@@ -1,3 +1,2 @@
/// <reference path="annotateWithTypeFromJSDoc.ts" />
/// <reference path="extractSymbol.ts" />
/// <reference path="useDefaultImport.ts" />

View File

@@ -1,89 +0,0 @@
/* @internal */
namespace ts.refactor.installTypesForPackage {
const actionName = "Convert to default import";
const description = getLocaleSpecificMessage(Diagnostics.Convert_to_default_import);
registerRefactor(actionName, { getEditsForAction, getAvailableActions });
function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
const { file, startPosition, program } = context;
if (!getAllowSyntheticDefaultImports(program.getCompilerOptions())) {
return undefined;
}
const importInfo = getConvertibleImportAtPosition(file, startPosition);
if (!importInfo) {
return undefined;
}
const module = getResolvedModule(file, importInfo.moduleSpecifier.text);
const resolvedFile = module && program.getSourceFile(module.resolvedFileName);
if (!(resolvedFile && resolvedFile.externalModuleIndicator && isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals)) {
return undefined;
}
return [
{
name: actionName,
description,
actions: [
{
description,
name: actionName,
},
],
},
];
}
function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined {
const { file, startPosition } = context;
Debug.assertEqual(actionName, _actionName);
const importInfo = getConvertibleImportAtPosition(file, startPosition);
if (!importInfo) {
return undefined;
}
const { importStatement, name, moduleSpecifier } = importInfo;
const newImportClause = createImportClause(name, /*namedBindings*/ undefined);
const newImportStatement = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, newImportClause, moduleSpecifier);
return {
edits: textChanges.ChangeTracker.with(context, t => t.replaceNode(file, importStatement, newImportStatement)),
renameFilename: undefined,
renameLocation: undefined,
};
}
function getConvertibleImportAtPosition(
file: SourceFile,
startPosition: number,
): { importStatement: AnyImportSyntax, name: Identifier, moduleSpecifier: StringLiteral } | undefined {
let node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false);
while (true) {
switch (node.kind) {
case SyntaxKind.ImportEqualsDeclaration:
const eq = node as ImportEqualsDeclaration;
const { moduleReference } = eq;
return moduleReference.kind === SyntaxKind.ExternalModuleReference && isStringLiteral(moduleReference.expression)
? { importStatement: eq, name: eq.name, moduleSpecifier: moduleReference.expression }
: undefined;
case SyntaxKind.ImportDeclaration:
const d = node as ImportDeclaration;
const { importClause } = d;
return importClause && !importClause.name && importClause.namedBindings.kind === SyntaxKind.NamespaceImport && isStringLiteral(d.moduleSpecifier)
? { importStatement: d, name: importClause.namedBindings.name, moduleSpecifier: d.moduleSpecifier }
: undefined;
// For known child node kinds of convertible imports, try again with parent node.
case SyntaxKind.NamespaceImport:
case SyntaxKind.ExternalModuleReference:
case SyntaxKind.ImportKeyword:
case SyntaxKind.Identifier:
case SyntaxKind.StringLiteral:
case SyntaxKind.AsteriskToken:
break;
default:
return undefined;
}
node = node.parent;
}
}
}

View File

@@ -25,6 +25,30 @@ namespace ts {
check(sourceFile);
}
if (getAllowSyntheticDefaultImports(program.getCompilerOptions())) {
for (const importNode of sourceFile.imports) {
const name = importNameForConvertToDefaultImport(importNode.parent);
if (!name) continue;
const module = getResolvedModule(sourceFile, importNode.text);
const resolvedFile = module && program.getSourceFile(module.resolvedFileName);
if (resolvedFile && resolvedFile.externalModuleIndicator && isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals) {
diags.push(createDiagnosticForNode(name, Diagnostics.Import_may_be_converted_to_a_default_import));
}
}
}
return diags.concat(checker.getSuggestionDiagnostics(sourceFile));
}
function importNameForConvertToDefaultImport(node: Node): Identifier | undefined {
if (isExternalModuleReference(node)) {
return node.parent.name;
}
if (isImportDeclaration(node)) {
const { importClause, moduleSpecifier } = node;
return importClause && !importClause.name && importClause.namedBindings.kind === SyntaxKind.NamespaceImport && isStringLiteral(moduleSpecifier)
? importClause.namedBindings.name
: undefined;
}
}
}

View File

@@ -0,0 +1,39 @@
/// <reference path='fourslash.ts' />
// @allowSyntheticDefaultImports: true
// @Filename: /a.d.ts
////declare const x: number;
////export = x;
// @Filename: /b.ts
////import * as [|a|] from "./a";
// @Filename: /c.ts
////import [|a|] = require("./a");
// @Filename: /d.ts
////import "./a";
// @Filename: /e.ts
////import * as n from "./non-existant";
for (const file of ["/b.ts", "/c.ts"]) {
goTo.file(file);
verify.getSuggestionDiagnostics([{
message: "Import may be converted to a default import.",
range: test.ranges().find(r => r.fileName === file),
code: 80003,
}]);
verify.codeFix({
description: "Convert to default import",
newFileContent: `import a from "./a";`,
});
}
for (const file of ["/d.ts", "/e.ts"]) {
goTo.file(file);
verify.getSuggestionDiagnostics([]);
}

View File

@@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// @allowSyntheticDefaultImports: true
// @Filename: /a.d.ts
////declare const x: number;
////export = x;
// @Filename: /b.ts
////import * as [|a1|] from "./a";
////import [|a2|] = require("./a");
goTo.file("/b.ts");
verify.codeFixAll({
fixId: "useDefaultImport",
// TODO: GH#22337
newFileContent: `import a1 from "./a";import a2 from "./a";`,
});

View File

@@ -13,7 +13,6 @@
verify.getSuggestionDiagnostics([{
message: "This constructor function may be converted to a class declaration.",
category: "suggestion",
code: 80002,
}]);

View File

@@ -9,7 +9,6 @@
verify.getSuggestionDiagnostics([{
message: "File is a CommonJS module; it may be converted to an ES6 module.",
category: "suggestion",
code: 80001,
}]);

View File

@@ -1,41 +0,0 @@
/// <reference path='fourslash.ts' />
// @allowSyntheticDefaultImports: true
// @Filename: /a.d.ts
////declare const x: number;
////export = x;
// @Filename: /b.ts
/////*b0*/import * as a from "./a";/*b1*/
// @Filename: /c.ts
/////*c0*/import a = require("./a");/*c1*/
// @Filename: /d.ts
/////*d0*/import "./a";/*d1*/
// @Filename: /e.ts
/////*e0*/import * as n from "./non-existant";/*e1*/
goTo.select("b0", "b1");
edit.applyRefactor({
refactorName: "Convert to default import",
actionName: "Convert to default import",
actionDescription: "Convert to default import",
newContent: 'import a from "./a";',
});
goTo.select("c0", "c1");
edit.applyRefactor({
refactorName: "Convert to default import",
actionName: "Convert to default import",
actionDescription: "Convert to default import",
newContent: 'import a from "./a";',
});
goTo.select("d0", "d1");
verify.not.applicableRefactorAvailableAtMarker("d0");
goTo.select("e0", "e1");
verify.not.applicableRefactorAvailableAtMarker("e0");