Cherry-pick #52565 to release-5.0 (#53055)

Co-authored-by: Oleksandr T <oleksandr.tarasiuk@outlook.com>
This commit is contained in:
Jake Bailey 2023-03-02 12:38:45 -08:00 committed by GitHub
parent 3ede9245ce
commit b0afbd6aaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 47 deletions

View File

@ -1,14 +1,16 @@
import {
CodeFixContextBase,
Diagnostics,
factory,
getSynthesizedDeepClone,
getSynthesizedDeepClones,
getTokenAtPosition,
ImportClause,
ImportDeclaration,
ImportSpecifier,
isImportDeclaration,
isImportSpecifier,
SourceFile,
textChanges,
TextSpan,
tryCast,
} from "../_namespaces/ts";
import {
codeFixAll,
@ -16,52 +18,64 @@ import {
registerCodeFix,
} from "../_namespaces/ts.codefix";
const errorCodes = [Diagnostics.This_import_is_never_used_as_a_value_and_must_use_import_type_because_importsNotUsedAsValues_is_set_to_error.code];
const errorCodes = [
Diagnostics.This_import_is_never_used_as_a_value_and_must_use_import_type_because_importsNotUsedAsValues_is_set_to_error.code,
Diagnostics._0_is_a_type_and_must_be_imported_using_a_type_only_import_when_verbatimModuleSyntax_is_enabled.code,
];
const fixId = "convertToTypeOnlyImport";
registerCodeFix({
errorCodes,
getCodeActions: function getCodeActionsToConvertToTypeOnlyImport(context) {
const changes = textChanges.ChangeTracker.with(context, t => {
const importDeclaration = getImportDeclarationForDiagnosticSpan(context.span, context.sourceFile);
fixSingleImportDeclaration(t, importDeclaration, context);
});
if (changes.length) {
const declaration = getDeclaration(context.sourceFile, context.span.start);
if (declaration) {
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, context.sourceFile, declaration));
return [createCodeFixAction(fixId, changes, Diagnostics.Convert_to_type_only_import, fixId, Diagnostics.Convert_all_imports_not_used_as_a_value_to_type_only_imports)];
}
return undefined;
},
fixIds: [fixId],
getAllCodeActions: function getAllCodeActionsToConvertToTypeOnlyImport(context) {
return codeFixAll(context, errorCodes, (changes, diag) => {
const importDeclaration = getImportDeclarationForDiagnosticSpan(diag, context.sourceFile);
fixSingleImportDeclaration(changes, importDeclaration, context);
const declaration = getDeclaration(diag.file, diag.start);
if (declaration) {
doChange(changes, diag.file, declaration);
}
});
}
});
function getImportDeclarationForDiagnosticSpan(span: TextSpan, sourceFile: SourceFile) {
return tryCast(getTokenAtPosition(sourceFile, span.start).parent, isImportDeclaration);
function getDeclaration(sourceFile: SourceFile, pos: number) {
const { parent } = getTokenAtPosition(sourceFile, pos);
return isImportSpecifier(parent) || isImportDeclaration(parent) && parent.importClause ? parent : undefined;
}
function fixSingleImportDeclaration(changes: textChanges.ChangeTracker, importDeclaration: ImportDeclaration | undefined, context: CodeFixContextBase) {
if (!importDeclaration?.importClause) {
return;
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, declaration: ImportDeclaration | ImportSpecifier) {
if (isImportSpecifier(declaration)) {
changes.replaceNode(sourceFile, declaration, factory.updateImportSpecifier(declaration, /*isTypeOnly*/ true, declaration.propertyName, declaration.name));
}
const { importClause } = importDeclaration;
// `changes.insertModifierBefore` produces a range that might overlap further changes
changes.insertText(context.sourceFile, importDeclaration.getStart() + "import".length, " type");
// `import type foo, { Bar }` is not allowed, so move `foo` to new declaration
if (importClause.name && importClause.namedBindings) {
changes.deleteNodeRangeExcludingEnd(context.sourceFile, importClause.name, importDeclaration.importClause.namedBindings);
changes.insertNodeBefore(context.sourceFile, importDeclaration, factory.updateImportDeclaration(
importDeclaration,
/*modifiers*/ undefined,
factory.createImportClause(
/*isTypeOnly*/ true,
importClause.name,
/*namedBindings*/ undefined),
importDeclaration.moduleSpecifier,
/*assertClause*/ undefined));
else {
const importClause = declaration.importClause as ImportClause;
if (importClause.name && importClause.namedBindings) {
changes.replaceNodeWithNodes(sourceFile, declaration, [
factory.createImportDeclaration(
getSynthesizedDeepClones(declaration.modifiers, /*includeTrivia*/ true),
factory.createImportClause(/*isTypeOnly*/ true, getSynthesizedDeepClone(importClause.name, /*includeTrivia*/ true), /*namedBindings*/ undefined),
getSynthesizedDeepClone(declaration.moduleSpecifier, /*includeTrivia*/ true),
getSynthesizedDeepClone(declaration.assertClause, /*includeTrivia*/ true),
),
factory.createImportDeclaration(
getSynthesizedDeepClones(declaration.modifiers, /*includeTrivia*/ true),
factory.createImportClause(/*isTypeOnly*/ true, /*name*/ undefined, getSynthesizedDeepClone(importClause.namedBindings, /*includeTrivia*/ true)),
getSynthesizedDeepClone(declaration.moduleSpecifier, /*includeTrivia*/ true),
getSynthesizedDeepClone(declaration.assertClause, /*includeTrivia*/ true),
),
]);
}
else {
const importDeclaration = factory.updateImportDeclaration(declaration, declaration.modifiers,
factory.updateImportClause(importClause, /*isTypeOnly*/ true, importClause.name, importClause.namedBindings), declaration.moduleSpecifier, declaration.assertClause);
changes.replaceNode(sourceFile, declaration, importDeclaration);
}
}
}

View File

@ -309,6 +309,7 @@ Info 32 [00:01:13.000] response:
"2713",
"1205",
"1371",
"1484",
"2690",
"2420",
"2720",
@ -720,7 +721,6 @@ Info 32 [00:01:13.000] response:
"1477",
"1478",
"1479",
"1484",
"1485",
"1486",
"2200",

View File

@ -9,8 +9,8 @@
// @Filename: imports.ts
////import {
//// B,
//// C,
//// B,
//// C,
////} from './exports';
////
////declare const b: B;
@ -19,11 +19,11 @@
goTo.file("imports.ts");
verify.codeFix({
index: 0,
description: ts.Diagnostics.Convert_to_type_only_import.message,
newFileContent: `import type {
B,
C,
index: 0,
description: ts.Diagnostics.Convert_to_type_only_import.message,
newFileContent: `import type {
B,
C,
} from './exports';
declare const b: B;

View File

@ -17,9 +17,9 @@
goTo.file("imports.ts");
verify.codeFix({
index: 0,
description: ts.Diagnostics.Convert_to_type_only_import.message,
newFileContent: `import type A from './exports';
index: 0,
description: ts.Diagnostics.Convert_to_type_only_import.message,
newFileContent: `import type A from './exports';
import type { B, C } from './exports';
declare const a: A;

View File

@ -25,9 +25,9 @@
goTo.file("imports.ts");
verify.codeFixAll({
fixId: "convertToTypeOnlyImport",
fixAllDescription: ts.Diagnostics.Convert_all_imports_not_used_as_a_value_to_type_only_imports.message,
newFileContent: `import type A from './exports1';
fixId: "convertToTypeOnlyImport",
fixAllDescription: ts.Diagnostics.Convert_all_imports_not_used_as_a_value_to_type_only_imports.message,
newFileContent: `import type A from './exports1';
import type { B, C } from './exports1';
import type D from "./exports2";
import type * as others from "./exports2";

View File

@ -0,0 +1,17 @@
/// <reference path="fourslash.ts" />
// @module: esnext
// @verbatimModuleSyntax: true
// @filename: /b.ts
////export interface I {}
////export const foo = {};
// @filename: /a.ts
////import { I, foo } from "./b";
goTo.file("/a.ts");
verify.codeFix({
index: 0,
description: ts.Diagnostics.Convert_to_type_only_import.message,
newFileContent: `import { type I, foo } from "./b";`
});