From 3c8e9854f64e0ac35b6998b2c47a5169580a38ad Mon Sep 17 00:00:00 2001 From: "Dr. Dominik Jain" Date: Sun, 9 Aug 2026 20:12:51 +0200 Subject: [PATCH] Improve identification of container/low-level symbols (#1834) This affects results returned for request_containing_symbol, some tests were adjusted accordingly. --- CHANGELOG.md | 8 +++--- src/serena/symbol.py | 2 +- src/solidlsp/ls.py | 27 ++++++------------ .../lsp_protocol_handler/lsp_types.py | 18 ++++++++++++ .../toml/test_toml_symbol_retrieval.py | 10 ++----- test/solidlsp/vue/test_vue_error_cases.py | 28 ------------------- .../solidlsp/vue/test_vue_symbol_retrieval.py | 11 ++++---- 7 files changed, 40 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8fa55c..cad9de8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,10 +97,10 @@ Status of the `main` branch. Changes prior to the next official version change w 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 - + struct bodies, interface bodies and `const` groups; improve the logic for finding the nearest + enclosing symbol, adding the helper function `SymbolKind.is_container` (which is now also + applied to identify high-level symbols that should appear in symbol overiews). + * JetBrains: - `jet_brains_find_symbol`: Disallow wildcard-only search, delegating to overview tool if request is for file diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 6a17813a..8c230ae5 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -247,7 +247,7 @@ class LanguageServerSymbol(Symbol, ToStringMixin): :return: whether the symbol is a low-level symbol (variable, constant, etc.), which typically represents data rather than structure and therefore is not relevant in a high-level overview of the code. """ - return self.symbol_kind >= SymbolKind.Variable.value + return not SymbolKind.is_container(self.symbol_kind) @property def overload_idx(self) -> int | None: diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index fb809e63..cba90723 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -48,6 +48,7 @@ from solidlsp.lsp_protocol_handler.lsp_types import ( LocationLink, RenameParams, SymbolInformation, + SymbolKind, ) from solidlsp.lsp_protocol_handler.server import ( LSPError, @@ -2590,16 +2591,6 @@ class SolidLanguageServer(ABC): location["relativePath"] = relative_file_path location["uri"] = Path(absolute_file_path).as_uri() - # 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"] end = range_d["end"] @@ -2615,17 +2606,15 @@ class SolidLanguageServer(ABC): column_condition = column >= start["character"] return line_condition and column_condition + def is_multiline_container(s): + return SymbolKind.is_container(s["kind"]) and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] + + def is_variable_or_constant(s): + return s["kind"] in {ls_types.SymbolKind.Variable, ls_types.SymbolKind.Constant} + # Only consider containers that are not one-liners (otherwise we may get imports) - candidate_containers = [ - s - 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"] - ] # 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) + candidate_containers = [s for s in document_symbols.iter_symbols() if is_multiline_container(s) or is_variable_or_constant(s)] if not candidate_containers: return None diff --git a/src/solidlsp/lsp_protocol_handler/lsp_types.py b/src/solidlsp/lsp_protocol_handler/lsp_types.py index c96f5fce..8dfc92e7 100644 --- a/src/solidlsp/lsp_protocol_handler/lsp_types.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_types.py @@ -182,6 +182,24 @@ class SymbolKind(IntEnum): def _missing_(cls, value: Any) -> "SymbolKind": return cls.Unknown + @staticmethod + def is_container(kind: "SymbolKind") -> bool: + """Returns True if the symbol kind is a container (i.e. can have children).""" + return kind in { + SymbolKind.File, + SymbolKind.Module, + SymbolKind.Namespace, + SymbolKind.Package, + SymbolKind.Class, + SymbolKind.Method, + SymbolKind.Constructor, + SymbolKind.Enum, + SymbolKind.Interface, + SymbolKind.Function, + SymbolKind.Object, + SymbolKind.Struct, + } + class SymbolTag(IntEnum): """Symbol tags are extra annotations that tweak the rendering of a symbol. diff --git a/test/solidlsp/toml/test_toml_symbol_retrieval.py b/test/solidlsp/toml/test_toml_symbol_retrieval.py index 9696784b..c2497c0b 100644 --- a/test/solidlsp/toml/test_toml_symbol_retrieval.py +++ b/test/solidlsp/toml/test_toml_symbol_retrieval.py @@ -24,17 +24,13 @@ class TestTomlSymbolRetrieval: @pytest.mark.parametrize("language_server", [LanguageServerId.TOML], indirect=True) @pytest.mark.parametrize("repo_path", [LanguageServerId.TOML], indirect=True) def test_request_containing_symbol_behavior(self, language_server: SolidLanguageServer, repo_path: Path) -> None: - """Test request_containing_symbol behavior for TOML files. - - Note: Taplo LSP doesn't support definition/containing symbol lookups for TOML files - since TOML is a configuration format, not code. This test verifies the behavior. + """ + Test request_containing_symbol behavior for TOML files. """ # Line 2 (0-indexed: 1) is inside the [package] table containing_symbol = language_server.request_containing_symbol("Cargo.toml", 1, 5) - # Taplo doesn't support containing symbol lookup - returns None - # This is expected behavior for a configuration file format - assert containing_symbol is None, "TOML LSP doesn't support containing symbol lookup" + assert "[package]" in containing_symbol["body"].get_text() @pytest.mark.parametrize("language_server", [LanguageServerId.TOML], indirect=True) @pytest.mark.parametrize("repo_path", [LanguageServerId.TOML], indirect=True) diff --git a/test/solidlsp/vue/test_vue_error_cases.py b/test/solidlsp/vue/test_vue_error_cases.py index 59bc98da..a7431e80 100644 --- a/test/solidlsp/vue/test_vue_error_cases.py +++ b/test/solidlsp/vue/test_vue_error_cases.py @@ -36,20 +36,6 @@ class TestVueInvalidPositions: assert result is None or result == {}, f"Negative line number should return None or empty dict, got: {result}" - @pytest.mark.parametrize("language_server", [LanguageServerId.VUE], indirect=True) - def test_negative_character_number(self, language_server: SolidLanguageServer) -> None: - """Test requesting containing symbol with negative character number. - - Expected behavior: Should return None or empty dict, not crash. - """ - file_path = os.path.join("src", "components", "CalculatorInput.vue") - - # Request containing symbol at invalid negative character - result = language_server.request_containing_symbol(file_path, 10, -1) - - # Should handle gracefully - return None or empty dict - assert result is None or result == {}, f"Negative character number should return None or empty dict, got: {result}" - @pytest.mark.parametrize("language_server", [LanguageServerId.VUE], indirect=True) def test_line_number_beyond_file_length(self, language_server: SolidLanguageServer) -> None: """Test requesting containing symbol beyond file length. @@ -67,20 +53,6 @@ class TestVueInvalidPositions: # Verify it's an index error for list access assert "list index out of range" in str(exc_info.value), f"Expected 'list index out of range' error, got: {exc_info.value}" - @pytest.mark.parametrize("language_server", [LanguageServerId.VUE], indirect=True) - def test_character_number_beyond_line_length(self, language_server: SolidLanguageServer) -> None: - """Test requesting containing symbol beyond line length. - - Expected behavior: Should return None or empty dict, not crash. - """ - file_path = os.path.join("src", "components", "CalculatorInput.vue") - - # Request containing symbol at character 99999 (way beyond line length) - result = language_server.request_containing_symbol(file_path, 10, 99999) - - # Should handle gracefully - return None or empty dict - assert result is None or result == {}, f"Character beyond line length should return None or empty dict, got: {result}" - @pytest.mark.parametrize("language_server", [LanguageServerId.VUE], indirect=True) def test_references_at_negative_line(self, language_server: SolidLanguageServer) -> None: """Test requesting references with negative line number.""" diff --git a/test/solidlsp/vue/test_vue_symbol_retrieval.py b/test/solidlsp/vue/test_vue_symbol_retrieval.py index 94856e60..2da121e2 100644 --- a/test/solidlsp/vue/test_vue_symbol_retrieval.py +++ b/test/solidlsp/vue/test_vue_symbol_retrieval.py @@ -2,6 +2,7 @@ import os import pytest +from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer from solidlsp.ls_config import LanguageServerId from solidlsp.ls_types import SymbolKind @@ -81,7 +82,7 @@ class TestVueSymbolRetrieval: ], f"Expected property/variable/function kind for computed, got {containing_symbol.get('kind')}" @pytest.mark.parametrize("language_server", [LanguageServerId.VUE], indirect=True) - def test_request_containing_symbol_no_containing_symbol(self, language_server: SolidLanguageServer) -> None: + def test_request_containing_symbol_import_in_script_tag(self, language_server: SolidLanguageServer) -> None: file_path = os.path.join("src", "components", "CalculatorInput.vue") # Position in the import statements at the top of the script setup @@ -92,10 +93,10 @@ class TestVueSymbolRetrieval: # Request containing symbol for a position in the imports containing_symbol = language_server.request_containing_symbol(file_path, import_line, import_character) - # Should return None or empty dictionary for positions without containing symbol - assert containing_symbol is None or containing_symbol == {}, ( - f"Expected None or empty dict for import position, got {containing_symbol}" - ) + # Containing symbol is the script tag + assert containing_symbol is not None + s = LanguageServerSymbol(containing_symbol) + assert s.get_name_path().startswith("script") @pytest.mark.parametrize("language_server", [LanguageServerId.VUE], indirect=True) def test_request_referencing_symbols_store_function(self, language_server: SolidLanguageServer) -> None: