From 991852aee6fa7e1876ba2315e46f2f3d939af4a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:36:30 +0000 Subject: [PATCH 1/2] Initial plan for issue From 544d17525a8dfdf44f1de1b74f863c59156b6451 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Jun 2025 23:44:41 +0000 Subject: [PATCH 2/2] Implement fix for duplicate imports in isolatedDeclarations quick fixes Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- .../fixMissingTypeAnnotationOnExports.ts | 145 ++++++++++++------ ...eAnnotationOnExports_noDuplicateImports.ts | 22 +++ 2 files changed, 124 insertions(+), 43 deletions(-) create mode 100644 tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports_noDuplicateImports.ts diff --git a/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts b/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts index a82ccb01f00ff..26db46a3a16f5 100644 --- a/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts +++ b/src/services/codefixes/fixMissingTypeAnnotationOnExports.ts @@ -1,12 +1,14 @@ -import { - createCodeFixAction, - createCombinedCodeActions, - createImportAdder, - eachDiagnostic, - registerCodeFix, - typeNodeToAutoImportableTypeNode, - typePredicateToAutoImportableTypeNode, - typeToMinimizedReferenceType, +import { + createCodeFixAction, + createCombinedCodeActions, + createImportAdder, + eachDiagnostic, + ImportAdder, + importSymbols, + registerCodeFix, + tryGetAutoImportableReferenceFromTypeNode, + typePredicateToAutoImportableTypeNode, + typeToMinimizedReferenceType, } from "../_namespaces/ts.codefix.js"; import { ArrayBindingPattern, @@ -38,15 +40,17 @@ import { findAncestor, FunctionDeclaration, GeneratedIdentifierFlags, - getEmitScriptTarget, - getSourceFileOfNode, - getSynthesizedDeepClone, - getTokenAtPosition, + getEmitScriptTarget, + getNameForExportedSymbol, + getSourceFileOfNode, + getSynthesizedDeepClone, + getTokenAtPosition, getTrailingCommentRanges, hasInitializer, hasSyntacticModifier, - Identifier, - InternalNodeBuilderFlags, + Identifier, + ImportDeclaration, + InternalNodeBuilderFlags, isArrayBindingPattern, isArrayLiteralExpression, isAssertionExpression, @@ -80,16 +84,19 @@ import { isValueSignatureDeclaration, isVariableDeclaration, ModifierFlags, - ModifierLike, - Node, + ModifierLike, + NamedImports, + NamespaceImport, + Node, NodeBuilderFlags, NodeFlags, ObjectBindingPattern, ObjectLiteralExpression, ParameterDeclaration, PropertyAccessExpression, - PropertyDeclaration, - setEmitFlags, + PropertyDeclaration, + ScriptTarget, + setEmitFlags, SignatureDeclaration, some, SourceFile, @@ -1097,22 +1104,22 @@ function withContext( return emptyInferenceResult; } - function typeToTypeNode(type: Type, enclosingDeclaration: Node, flags = NodeBuilderFlags.None): TypeNode | undefined { - let isTruncated = false; - const minimizedTypeNode = typeToMinimizedReferenceType(typeChecker, type, enclosingDeclaration, declarationEmitNodeBuilderFlags | flags, declarationEmitInternalNodeBuilderFlags, { - moduleResolverHost: program, - trackSymbol() { - return true; - }, - reportTruncationError() { - isTruncated = true; - }, - }); - if (!minimizedTypeNode) { - return undefined; - } - const result = typeNodeToAutoImportableTypeNode(minimizedTypeNode, importAdder, scriptTarget); - return isTruncated ? factory.createKeywordTypeNode(SyntaxKind.AnyKeyword) : result; + function typeToTypeNode(type: Type, enclosingDeclaration: Node, flags = NodeBuilderFlags.None): TypeNode | undefined { + let isTruncated = false; + const minimizedTypeNode = typeToMinimizedReferenceType(typeChecker, type, enclosingDeclaration, declarationEmitNodeBuilderFlags | flags, declarationEmitInternalNodeBuilderFlags, { + moduleResolverHost: program, + trackSymbol() { + return true; + }, + reportTruncationError() { + isTruncated = true; + }, + }); + if (!minimizedTypeNode) { + return undefined; + } + const result = typeNodeToAutoImportableTypeNodeWithExistingImportCheck(minimizedTypeNode, importAdder, scriptTarget, sourceFile, typeChecker); + return isTruncated ? factory.createKeywordTypeNode(SyntaxKind.AnyKeyword) : result; } function typePredicateToTypeNode(typePredicate: TypePredicate, enclosingDeclaration: Node, flags = NodeBuilderFlags.None): TypeNode | undefined { @@ -1142,14 +1149,66 @@ function withContext( } } - function typeToStringForDiag(node: Node) { - setEmitFlags(node, EmitFlags.SingleLine); - const result = typePrinter.printNode(EmitHint.Unspecified, node, sourceFile); - if (result.length > defaultMaximumTruncationLength) { - return result.substring(0, defaultMaximumTruncationLength - "...".length) + "..."; - } - setEmitFlags(node, EmitFlags.None); - return result; + function typeToStringForDiag(node: Node) { + setEmitFlags(node, EmitFlags.SingleLine); + const result = typePrinter.printNode(EmitHint.Unspecified, node, sourceFile); + if (result.length > defaultMaximumTruncationLength) { + return result.substring(0, defaultMaximumTruncationLength - "...".length) + "..."; + } + setEmitFlags(node, EmitFlags.None); + return result; + } + + function typeNodeToAutoImportableTypeNodeWithExistingImportCheck(typeNode: TypeNode, importAdder: ImportAdder, scriptTarget: ScriptTarget, sourceFile: SourceFile, typeChecker: TypeChecker): TypeNode | undefined { + const importableReference = tryGetAutoImportableReferenceFromTypeNode(typeNode, scriptTarget); + if (importableReference) { + // Check if symbols are already available before importing them + const symbolsToImport = importableReference.symbols.filter(symbol => { + const symbolName = getNameForExportedSymbol(symbol, scriptTarget); + return !isSymbolAlreadyAvailable(symbolName, sourceFile, typeChecker); + }); + + if (symbolsToImport.length > 0) { + importSymbols(importAdder, symbolsToImport); + } + typeNode = importableReference.typeNode; + } + + // Ensure nodes are fresh so they can have different positions when going through formatting. + return getSynthesizedDeepClone(typeNode); + } + + function isSymbolAlreadyAvailable(symbolName: string, sourceFile: SourceFile, _typeChecker: TypeChecker): boolean { + // Check if the symbol name is already imported in the current file + for (const statement of sourceFile.statements) { + if (statement.kind === SyntaxKind.ImportDeclaration) { + const importDecl = statement as ImportDeclaration; + if (importDecl.importClause) { + // Check default import + if (importDecl.importClause.name && importDecl.importClause.name.text === symbolName) { + return true; + } + // Check named imports + if (importDecl.importClause.namedBindings && importDecl.importClause.namedBindings.kind === SyntaxKind.NamedImports) { + const namedImports = importDecl.importClause.namedBindings; + for (const element of namedImports.elements) { + const name = element.name.text; + if (name === symbolName) { + return true; + } + } + } + // Check namespace import + if (importDecl.importClause.namedBindings && importDecl.importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { + const namespaceImport = importDecl.importClause.namedBindings; + if (namespaceImport.name.text === symbolName) { + return true; + } + } + } + } + } + return false; } // Some --isolatedDeclarations errors are not present on the node that directly needs type annotation, so look in the diff --git a/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports_noDuplicateImports.ts b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports_noDuplicateImports.ts new file mode 100644 index 0000000000000..5c2925921d2b6 --- /dev/null +++ b/tests/cases/fourslash/codeFixMissingTypeAnnotationOnExports_noDuplicateImports.ts @@ -0,0 +1,22 @@ +/// + +// @isolatedDeclarations: true +// @declaration: true + +// @fileName: mymodule.d.ts +////declare class VolumeClass { +//// constructor(); +////} +////export const Volume: typeof VolumeClass; + +// @fileName: test.ts +////import { Volume } from './mymodule'; +////export const foo = new Volume(); + +verify.codeFixAll({ + fixId: "fixMissingTypeAnnotationOnExports", + fixAllDescription: ts.Diagnostics.Add_all_missing_type_annotations.message, + newFileContent: +`import { Volume } from './mymodule'; +export const foo: Volume = new Volume();` +}); \ No newline at end of file