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.
This commit is contained in:
xor authored and GitHub committed 2026-08-09 16:07:20 +02:00
1 parent bdc9a2de2d
commit c9f0ab80f1
4 files changed
+101 -7

No files matched your search

+4
View File
@@ -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
+17 -6
View File
@@ -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:
@@ -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
}
+59 -1
View File
@@ -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)