From c9f0ab80f117505121999eb2229902e5db6dc096 Mon Sep 17 00:00:00 2001 From: xor <127287135+xormania@users.noreply.github.com> Date: Sun, 9 Aug 2026 10:07:20 -0400 Subject: [PATCH] SolidLSP: admit Struct, Interface and Constant symbols as reference containers (#1831) find_referencing_symbols degraded to file-level attribution for any reference located inside a Go struct body, interface body or const group: request_containing_symbol found no admissible container, so request_referencing_symbols logged "Could not find containing symbol ... Returning file symbol instead" and fell back to the file symbol, losing the answer the tool exists to give. Root cause: the candidate-container filter admitted only Python's container kinds {Method, Function, Class} (plus Variable via the one-liner-exempt path). The language server is not at fault: hierarchicalDocumentSymbolSupport is declared and the DocumentSymbol tree is complete, but Go structs arrive as SymbolKind.Struct, interfaces as Interface and const-group members as Constant - none of which were admitted, so every candidate was rejected. Fix, mirroring the existing structure of the filter: - Struct and Interface join the multi-line container whitelist, - Constant joins Variable on the one-liner-exempt path (const-group members are one-liners by nature, exactly like the module-level variables that path already serves). The Python suite shows no attribution change (test/solidlsp/python/test_symbol_retrieval.py: 80 passed). Languages whose containers use the added kinds (e.g. Rust and C structs, TypeScript interfaces) get the same admission; verified for Go and Python only. Regression coverage: new Go fixture containment_sample.go with a const-iota group, a struct field and an interface method all referencing a named type; four tests assert containing-symbol and referencing-symbol attribution, and all four fail against the pre-change ls.py (verified). Full go-marker suite: 21 passed, 1 skipped. --- CHANGELOG.md | 4 ++ src/solidlsp/ls.py | 23 +++++-- .../repos/go/test_repo/containment_sample.go | 21 +++++++ test/solidlsp/go/test_go_basic.py | 60 ++++++++++++++++++- 4 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 test/resources/repos/go/test_repo/containment_sample.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a24fc9bc..21f5744f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,10 @@ Status of the `main` branch. Changes prior to the next official version change w `Deno.*` globals, which the plain TypeScript language server does not. Overlaps TypeScript on file extensions, so it is not auto-detected and must be selected explicitly; requires the `deno` CLI on PATH + - Fix: `find_referencing_symbols` reported file-level containers for references located inside Go + struct bodies, interface bodies and `const` groups, because containing-symbol detection admitted + only Python's container kinds; `Struct`, `Interface` and `Constant` symbols are now admitted as + containers * JetBrains: - `jet_brains_find_symbol`: Disallow wildcard-only search, delegating to overview tool if request is for file diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index d0706a72..fb809e63 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -2533,8 +2533,9 @@ class SolidLanguageServer(ABC): ) -> ls_types.UnifiedSymbolInformation | None: """ Finds the first symbol containing the position for the given file. - For Python, container symbols are considered to be those with kinds corresponding to - functions, methods, or classes (typically: Function (12), Method (6), Class (5)). + Container symbols are those with kinds corresponding to functions, methods, classes, structs + and interfaces, as well as variables and constants (which may be one-liners, e.g. members + of a Go ``const`` group). The method operates as follows: - Request the document symbols for the file. @@ -2546,7 +2547,7 @@ class SolidLanguageServer(ABC): among those above the given line. - If no container candidates are found, return None. - :param relative_file_path: The relative path to the Python file. + :param relative_file_path: The relative path to the file. :param line: The 0-indexed line number. :param column: The 0-indexed column (also called character). If not passed, the lookup will be based only on the line. @@ -2589,8 +2590,15 @@ class SolidLanguageServer(ABC): location["relativePath"] = relative_file_path location["uri"] = Path(absolute_file_path).as_uri() - # Allowed container kinds, currently only for Python - container_symbol_kinds = {ls_types.SymbolKind.Method, ls_types.SymbolKind.Function, ls_types.SymbolKind.Class} + # allowed container kinds; Struct and Interface cover languages (e.g. Go, Rust, C) whose + # containers are not reported as classes + container_symbol_kinds = { + ls_types.SymbolKind.Method, + ls_types.SymbolKind.Function, + ls_types.SymbolKind.Class, + ls_types.SymbolKind.Struct, + ls_types.SymbolKind.Interface, + } def is_position_in_range(line: int, range_d: ls_types.Range) -> bool: start = range_d["start"] @@ -2613,7 +2621,10 @@ class SolidLanguageServer(ABC): for s in document_symbols.iter_symbols() if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] ] - var_containers = [s for s in document_symbols.iter_symbols() if s["kind"] == ls_types.SymbolKind.Variable] + # variables and constants are admitted even as one-liners (e.g. members of a Go const group) + var_containers = [ + s for s in document_symbols.iter_symbols() if s["kind"] in {ls_types.SymbolKind.Variable, ls_types.SymbolKind.Constant} + ] candidate_containers.extend(var_containers) if not candidate_containers: diff --git a/test/resources/repos/go/test_repo/containment_sample.go b/test/resources/repos/go/test_repo/containment_sample.go new file mode 100644 index 00000000..c8ccc861 --- /dev/null +++ b/test/resources/repos/go/test_repo/containment_sample.go @@ -0,0 +1,21 @@ +package main + +type Severity int + +const ( + SevLow Severity = iota + SevHigh +) + +type Alert struct { + Level Severity + Msg string +} + +func (a Alert) IsHigh() bool { + return a.Level == SevHigh +} + +type Notifier interface { + Notify(level Severity) error +} diff --git a/test/solidlsp/go/test_go_basic.py b/test/solidlsp/go/test_go_basic.py index d623eb87..3f8f9b04 100644 --- a/test/solidlsp/go/test_go_basic.py +++ b/test/solidlsp/go/test_go_basic.py @@ -4,11 +4,13 @@ from pathlib import Path import pytest from serena.symbol import LanguageServerSymbol +from serena.util.text_utils import find_text_coordinates from solidlsp import SolidLanguageServer from solidlsp.ls_config import LanguageServerId +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils from test.conftest import find_identifier_position, get_repo_path, ls_has_verified_implementation_support -from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, read_repo_file, request_all_symbols @pytest.mark.go @@ -88,6 +90,62 @@ class TestGoLanguageServer: assert body.startswith(name), f"Expected grouped var {name} body to start with the identifier, got {body[:24]!r}" assert not body.startswith("var"), f"Grouped var {name} body must not include the 'var' keyword" + @pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True) + def test_request_containing_symbol_const_group_member(self, language_server: SolidLanguageServer) -> None: + """A reference inside a ``const`` group must be attributed to the enclosing constant.""" + file_path = os.path.join("containment_sample.go") + file_content = read_repo_file(language_server, file_path) + coords = find_text_coordinates(file_content, r"SevLow (Severity) = iota") + assert coords is not None, "Could not find the Severity reference in the const group" + containing_symbol = language_server.request_containing_symbol(file_path, coords.line, coords.col) + assert containing_symbol is not None, "Expected a containing symbol for a reference inside a const group" + assert containing_symbol["name"] == "SevLow" + assert containing_symbol["kind"] == SymbolKind.Constant + + @pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True) + def test_request_containing_symbol_struct_field(self, language_server: SolidLanguageServer) -> None: + """A reference in a struct field declaration must be attributed to the enclosing struct.""" + file_path = os.path.join("containment_sample.go") + file_content = read_repo_file(language_server, file_path) + coords = find_text_coordinates(file_content, r"Level (Severity)") + assert coords is not None, "Could not find the Severity reference in the struct field" + containing_symbol = language_server.request_containing_symbol(file_path, coords.line, coords.col) + assert containing_symbol is not None, "Expected a containing symbol for a reference inside a struct body" + assert containing_symbol["name"] == "Alert" + assert containing_symbol["kind"] == SymbolKind.Struct + + @pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True) + def test_request_containing_symbol_interface_method(self, language_server: SolidLanguageServer) -> None: + """A reference in an interface method signature must be attributed to the enclosing interface.""" + file_path = os.path.join("containment_sample.go") + file_content = read_repo_file(language_server, file_path) + coords = find_text_coordinates(file_content, r"Notify\(level (Severity)\)") + assert coords is not None, "Could not find the Severity reference in the interface method" + containing_symbol = language_server.request_containing_symbol(file_path, coords.line, coords.col) + assert containing_symbol is not None, "Expected a containing symbol for a reference inside an interface body" + assert containing_symbol["name"] == "Notifier" + assert containing_symbol["kind"] == SymbolKind.Interface + + @pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True) + def test_request_referencing_symbols_attributes_containers(self, language_server: SolidLanguageServer) -> None: + """References to a type used in a const group, a struct field and an interface method must + be attributed to the constant, the struct and the interface respectively, not to the file. + """ + file_path = os.path.join("containment_sample.go") + all_symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() + severity_symbol = next((sym for sym in all_symbols if sym.get("name") == "Severity"), None) + assert severity_symbol is not None, "Could not find the 'Severity' type symbol in containment_sample.go" + sel_start = severity_symbol["selectionRange"]["start"] + ref_symbols = [ + ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"]) + ] + assert ref_symbols, "Expected references to the Severity type" + ref_names = {ref["name"] for ref in ref_symbols} + assert "SevLow" in ref_names + assert "Alert" in ref_names + assert "Notifier" in ref_names + assert all(ref["kind"] != SymbolKind.File for ref in ref_symbols), f"File-level fallback attribution in {ref_names}" + if ls_has_verified_implementation_support(LanguageServerId.GO): @pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)