diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index b8b131ca3b56f..2948df6c18c83 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -653,6 +653,35 @@ function getDefinitionFromSymbol(typeChecker: TypeChecker, symbol: Symbol, node: } } +/** + * Gets the ScriptElementKind for a specific declaration. + * For merged declarations, this ensures each declaration returns its own kind + * rather than using the combined symbol flags. + * + * Fixes #22467: getDefinitionAtPosition doesn't distinguish different kinds in a merged declaration + */ +function getKindFromDeclaration(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node): ScriptElementKind { + // Check the specific declaration's syntax kind first + // This handles merged declarations correctly (e.g., namespace A + class A + interface A) + switch (declaration.kind) { + case SyntaxKind.ClassDeclaration: + return ScriptElementKind.classElement; + case SyntaxKind.ClassExpression: + return ScriptElementKind.localClassElement; + case SyntaxKind.InterfaceDeclaration: + return ScriptElementKind.interfaceElement; + case SyntaxKind.ModuleDeclaration: + return ScriptElementKind.moduleElement; + case SyntaxKind.EnumDeclaration: + return ScriptElementKind.enumElement; + case SyntaxKind.TypeAliasDeclaration: + return ScriptElementKind.typeElement; + // For other declaration types, fall back to the existing symbol-based logic + default: + return SymbolDisplay.getSymbolKind(checker, symbol, node); + } +} + /** * Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. * @@ -660,7 +689,7 @@ function getDefinitionFromSymbol(typeChecker: TypeChecker, symbol: Symbol, node: */ export function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node, unverified?: boolean, failedAliasResolution?: boolean): DefinitionInfo { const symbolName = checker.symbolToString(symbol); // Do not get scoped name, just the name of the symbol - const symbolKind = SymbolDisplay.getSymbolKind(checker, symbol, node); + const symbolKind = getKindFromDeclaration(declaration, checker, symbol, node); const containerName = symbol.parent ? checker.symbolToString(symbol.parent, node) : ""; return createDefinitionInfoFromName(checker, declaration, symbolKind, symbolName, containerName, unverified, failedAliasResolution); } diff --git a/tests/baselines/reference/goToDefinitionMergedDeclarations.baseline.jsonc b/tests/baselines/reference/goToDefinitionMergedDeclarations.baseline.jsonc new file mode 100644 index 0000000000000..c1e7dd50a83a9 --- /dev/null +++ b/tests/baselines/reference/goToDefinitionMergedDeclarations.baseline.jsonc @@ -0,0 +1,42 @@ +// === goToDefinition === +// === /tests/cases/fourslash/test.ts === +// <|namespace [|{| defId: 0 |}A|] { +// export interface B {} +// }|> +// <|class [|{| defId: 1 |}A|] {}|> +// <|interface [|{| defId: 2 |}A|] {}|> +// var x: /*GOTO DEF*/A; + + // === Details === + [ + { + "defId": 0, + "kind": "module", + "name": "A", + "containerName": "", + "isLocal": false, + "isAmbient": false, + "unverified": false, + "failedAliasResolution": false + }, + { + "defId": 1, + "kind": "class", + "name": "A", + "containerName": "", + "isLocal": false, + "isAmbient": false, + "unverified": false, + "failedAliasResolution": false + }, + { + "defId": 2, + "kind": "interface", + "name": "A", + "containerName": "", + "isLocal": false, + "isAmbient": false, + "unverified": false, + "failedAliasResolution": false + } + ] \ No newline at end of file diff --git a/tests/baselines/reference/goToDefinitionThis.baseline.jsonc b/tests/baselines/reference/goToDefinitionThis.baseline.jsonc index 202c0fe42a1d1..c48637116bed3 100644 --- a/tests/baselines/reference/goToDefinitionThis.baseline.jsonc +++ b/tests/baselines/reference/goToDefinitionThis.baseline.jsonc @@ -36,7 +36,7 @@ // === Details === [ { - "kind": "parameter", + "kind": "class", "name": "C", "containerName": "", "isLocal": false, diff --git a/tests/baselines/reference/goToDefinitionTypeofThis.baseline.jsonc b/tests/baselines/reference/goToDefinitionTypeofThis.baseline.jsonc index 52f9b8cec9473..5c21611bff959 100644 --- a/tests/baselines/reference/goToDefinitionTypeofThis.baseline.jsonc +++ b/tests/baselines/reference/goToDefinitionTypeofThis.baseline.jsonc @@ -36,7 +36,7 @@ // === Details === [ { - "kind": "parameter", + "kind": "class", "name": "C", "containerName": "", "isLocal": false, diff --git a/tests/baselines/reference/goToTypeDefinition4.baseline.jsonc b/tests/baselines/reference/goToTypeDefinition4.baseline.jsonc index 1d957d23a8f45..f6af0bae3019c 100644 --- a/tests/baselines/reference/goToTypeDefinition4.baseline.jsonc +++ b/tests/baselines/reference/goToTypeDefinition4.baseline.jsonc @@ -18,7 +18,7 @@ [ { "defId": 0, - "kind": "const", + "kind": "type", "name": "T", "containerName": "\"/tests/cases/fourslash/foo\"", "isLocal": false, diff --git a/tests/baselines/reference/goToTypeDefinition_arrayType.baseline.jsonc b/tests/baselines/reference/goToTypeDefinition_arrayType.baseline.jsonc index 1cd2c60375f26..c0ef7ddf57a03 100644 --- a/tests/baselines/reference/goToTypeDefinition_arrayType.baseline.jsonc +++ b/tests/baselines/reference/goToTypeDefinition_arrayType.baseline.jsonc @@ -229,7 +229,7 @@ }, { "defId": 1, - "kind": "var", + "kind": "interface", "name": "Array", "containerName": "", "isLocal": false, @@ -486,7 +486,7 @@ }, { "defId": 1, - "kind": "var", + "kind": "interface", "name": "Array", "containerName": "", "isLocal": false, diff --git a/tests/cases/fourslash/goToDefinitionMergedDeclarations.ts b/tests/cases/fourslash/goToDefinitionMergedDeclarations.ts new file mode 100644 index 0000000000000..7be218e0677b6 --- /dev/null +++ b/tests/cases/fourslash/goToDefinitionMergedDeclarations.ts @@ -0,0 +1,15 @@ +/// + +// @Filename: test.ts +/////*namespaceDecl*/namespace A { +//// export interface B {} +////} +/////*classDecl*/class A {} +/////*interfaceDecl*/interface A {} +////var x: /*usage*/A; + +// Test that getDefinitionAtPosition returns the correct kind for each merged declaration +// Issue #22467: Before fix, all three return 'class' (bug) +// After fix: Returns 'module', 'class', 'interface' respectively + +verify.baselineGoToDefinition("usage");