mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-04-17 01:49:41 -05:00
Convert use-default-import refactor to a codefix (#22334)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -20,3 +20,4 @@
|
||||
/// <reference path='inferFromUsage.ts' />
|
||||
/// <reference path="fixInvalidImportSyntax.ts" />
|
||||
/// <reference path="fixStrictClassInitialization.ts" />
|
||||
/// <reference path="useDefaultImport.ts" />
|
||||
|
||||
43
src/services/codefixes/useDefaultImport.ts
Normal file
43
src/services/codefixes/useDefaultImport.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
/// <reference path="annotateWithTypeFromJSDoc.ts" />
|
||||
/// <reference path="extractSymbol.ts" />
|
||||
/// <reference path="useDefaultImport.ts" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
39
tests/cases/fourslash/codeFixUseDefaultImport.ts
Normal file
39
tests/cases/fourslash/codeFixUseDefaultImport.ts
Normal 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([]);
|
||||
}
|
||||
18
tests/cases/fourslash/codeFixUseDefaultImport_all.ts
Normal file
18
tests/cases/fourslash/codeFixUseDefaultImport_all.ts
Normal 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";`,
|
||||
});
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
verify.getSuggestionDiagnostics([{
|
||||
message: "This constructor function may be converted to a class declaration.",
|
||||
category: "suggestion",
|
||||
code: 80002,
|
||||
}]);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
verify.getSuggestionDiagnostics([{
|
||||
message: "File is a CommonJS module; it may be converted to an ES6 module.",
|
||||
category: "suggestion",
|
||||
code: 80001,
|
||||
}]);
|
||||
|
||||
|
||||
@@ -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");
|
||||
Reference in New Issue
Block a user