diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fcb93c45dc1dd..501d7a21a1624 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -48121,6 +48121,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { checkAliasSymbol(node); if (node.kind === SyntaxKind.ImportSpecifier) { checkModuleExportName(node.propertyName); + + // Check for duplicate import names + const importSpecifier = node; + const namecheck = getTextOfIdentifierOrLiteral(importSpecifier.name); + const propertyNamecheck = importSpecifier.propertyName ? getTextOfIdentifierOrLiteral(importSpecifier.propertyName) : ""; + + if (namecheck === propertyNamecheck) { + errorOrSuggestion(/*isError*/ false, importSpecifier, Diagnostics.Redundant_named_import_0, namecheck); + } + if ( moduleExportNameIsDefault(node.propertyName || node.name) && getESModuleInterop(compilerOptions) && diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 0d7e0bb39a8d4..1cf1d35502746 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -5337,6 +5337,11 @@ "category": "Error", "code": 6189 }, + "Redundant named import '{0}'.": { + "category": "Suggestion", + "code": 6190, + "reportsUnnecessary": true + }, "Whether to keep outdated console output in watch mode instead of clearing the screen.": { "category": "Message", "code": 6191 @@ -8267,6 +8272,14 @@ "category": "Message", "code": 95197 }, + "Simplify redundant import '{0}'": { + "category": "Message", + "code": 95198 + }, + "Simplify all redundant imports": { + "category": "Message", + "code": 95199 + }, "No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": { "category": "Error", diff --git a/src/services/_namespaces/ts.codefix.ts b/src/services/_namespaces/ts.codefix.ts index 3cf05630388de..2aecc1ece0566 100644 --- a/src/services/_namespaces/ts.codefix.ts +++ b/src/services/_namespaces/ts.codefix.ts @@ -74,3 +74,4 @@ export * from "../codefixes/splitTypeOnlyImport.js"; export * from "../codefixes/convertConstToLet.js"; export * from "../codefixes/fixExpectedComma.js"; export * from "../codefixes/fixAddVoidToPromise.js"; +export * from "../codefixes/removeUnnecessaryNamedImport.js"; diff --git a/src/services/codefixes/removeUnnecessaryNamedImport.ts b/src/services/codefixes/removeUnnecessaryNamedImport.ts new file mode 100644 index 0000000000000..7c711b4927259 --- /dev/null +++ b/src/services/codefixes/removeUnnecessaryNamedImport.ts @@ -0,0 +1,53 @@ +import { + codeFixAll, + createCodeFixAction, + registerCodeFix, +} from "../_namespaces/ts.codefix.js"; +import { + Diagnostics, + factory, + getTokenAtPosition, + isImportSpecifier, + SourceFile, + textChanges, + TextSpan, + tryCast, +} from "../_namespaces/ts.js"; + +const fixId = "removeUnnecessaryNamedImport"; +const errorCodes = [ + Diagnostics.Redundant_named_import_0.code, +]; + +registerCodeFix({ + errorCodes, + getCodeActions: function getCodeActionsToRemoveUnnecessaryNamedImport(context) { + const changes = textChanges.ChangeTracker.with(context, t => makeChange(t, context.sourceFile, context.span)); + const token = getTokenAtPosition(context.sourceFile, context.span.start); + const importSpecifier = tryCast(token.parent, isImportSpecifier); + + if (!importSpecifier) { + return; + } + + if (changes.length > 0) { + return [createCodeFixAction(fixId, changes, [Diagnostics.Simplify_redundant_import_0, importSpecifier.name.text], fixId, Diagnostics.Simplify_all_redundant_imports)]; + } + }, + fixIds: [fixId], + getAllCodeActions: context => { + return codeFixAll(context, errorCodes, (changes, diag) => makeChange(changes, diag.file, diag)); + }, +}); + +function makeChange(changeTracker: textChanges.ChangeTracker, sourceFile: SourceFile, span: TextSpan) { + const token = getTokenAtPosition(sourceFile, span.start); + const importSpecifier = tryCast(token.parent, isImportSpecifier); + if (!importSpecifier) { + return; + } + + if (importSpecifier.propertyName && importSpecifier.propertyName.text === importSpecifier.name.text) { + changeTracker.replaceNode(sourceFile, importSpecifier, factory.updateImportSpecifier(importSpecifier, importSpecifier.isTypeOnly, /*propertyName*/ undefined, importSpecifier.name)); + } +} diff --git a/tests/cases/fourslash/codeFixRemoveUnnecessaryNamedImports.ts b/tests/cases/fourslash/codeFixRemoveUnnecessaryNamedImports.ts new file mode 100644 index 0000000000000..a8f18c080da28 --- /dev/null +++ b/tests/cases/fourslash/codeFixRemoveUnnecessaryNamedImports.ts @@ -0,0 +1,31 @@ +/// + +// @filename: /a.ts +////export const foo = () => console.log('Hello world!'); +////export const bar = () => console.log('Hello world!'); + +// @filename: /b.ts +////import { foo as foo, bar as bar } from "./a"; +////foo(); +////bar(); + +goTo.file("/b.ts"); + +verify.codeFix({ + description: 'Simplify redundant import \'foo\'', + index: 0, + newFileContent: +`import { foo, bar as bar } from "./a"; +foo(); +bar();` +}); + +verify.codeFixAll({ + fixAllDescription: ts.Diagnostics.Simplify_all_redundant_imports.message, + fixId: "removeUnnecessaryNamedImport", + newFileContent: +`import { foo, bar } from "./a"; +foo(); +bar();` +}); + \ No newline at end of file diff --git a/tests/cases/fourslash/redundantImportDiagnostic.ts b/tests/cases/fourslash/redundantImportDiagnostic.ts new file mode 100644 index 0000000000000..993ed8d79f5e4 --- /dev/null +++ b/tests/cases/fourslash/redundantImportDiagnostic.ts @@ -0,0 +1,21 @@ +/// + +// @filename: /a.ts +//// export const bar = () => console.log('Hello world!'); + +// @filename: /b.ts +//// import { bar as bar } from "./a"; +//// bar(); + + +goTo.file("/b.ts"); + + +verify.getSuggestionDiagnostics([ + { + message: "Redundant named import 'bar'.", + code: ts.Diagnostics.Redundant_named_import_0.code, + range: {pos: 9, end: 19, fileName:"/b.ts"}, + reportsUnnecessary: true, + } +]); \ No newline at end of file