mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 06:17:58 +00:00
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:
4 files changed
+101
-7
No files matched your search
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user