diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e757f030..1f20f585 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -566,7 +566,7 @@ jobs: df -h - name: Test with pytest shell: bash - run: uv run poe test + run: uv run poe test -q --tb=short - name: Type-checking with mypy shell: bash run: uv run poe type-check diff --git a/src/solidlsp/language_servers/al_language_server.py b/src/solidlsp/language_servers/al_language_server.py index 43cf1ab3..ed3cf9c7 100644 --- a/src/solidlsp/language_servers/al_language_server.py +++ b/src/solidlsp/language_servers/al_language_server.py @@ -1013,9 +1013,18 @@ class ALLanguageServer(SolidLanguageServer): original_name = symbol["name"] normalized_name = self._extract_al_display_name(original_name) - # Store original name if it was normalized (for hover injection) + if symbol.get("kind") in (SymbolKind.Function, SymbolKind.Method) and "(" in normalized_name: + normalized_name = normalized_name.split("(", 1)[0].strip() + + if symbol.get("kind") == SymbolKind.Method and normalized_name.lower().startswith("action "): + normalized_name = normalized_name.split(None, 1)[-1].strip() + + if symbol.get("kind") == SymbolKind.Field and ":" in normalized_name: + normalized_name = normalized_name.split(":", 1)[0].strip() + + # Store original name if it was normalized for an AL object declaration # Only store if we have valid position data to avoid false matches at (0, 0) - if original_name != normalized_name: + if original_name != normalized_name and self._AL_OBJECT_NAME_PATTERN.match(original_name): sel_range = symbol.get("selectionRange") if sel_range: start = sel_range.get("start") # type: ignore @@ -1026,6 +1035,11 @@ class ALLanguageServer(SolidLanguageServer): return normalized_name + @override + def _document_symbols_cache_fingerprint(self) -> int: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + @override def request_hover( self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None diff --git a/src/solidlsp/language_servers/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server.py index 406c06e0..11f78106 100644 --- a/src/solidlsp/language_servers/dart_language_server.py +++ b/src/solidlsp/language_servers/dart_language_server.py @@ -1,9 +1,12 @@ import logging import os import pathlib +from collections.abc import Hashable from typing import cast -from solidlsp.ls import SolidLanguageServer +from overrides import override + +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings @@ -34,6 +37,15 @@ class DartLanguageServer(SolidLanguageServer): config, repository_root_path, ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), "dart", solidlsp_settings ) + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + return symbol["name"].rsplit(".", 1)[-1] + @classmethod def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str: dart_settings = solidlsp_settings.get_ls_specific_settings(Language.DART) diff --git a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py index d200c899..688da5e8 100644 --- a/src/solidlsp/language_servers/elixir_tools/elixir_tools.py +++ b/src/solidlsp/language_servers/elixir_tools/elixir_tools.py @@ -4,11 +4,12 @@ import pathlib import stat import subprocess import threading +from collections.abc import Hashable from typing import Any, cast from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -215,6 +216,15 @@ class ElixirTools(SolidLanguageServer): # Set generous timeout for Expert which can be slow to initialize and respond self.set_request_timeout(180.0) + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + return symbol["name"].removeprefix("defp ").removeprefix("def ").split("(", 1)[0].strip() + @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ diff --git a/src/solidlsp/language_servers/gopls.py b/src/solidlsp/language_servers/gopls.py index 40768842..c855b836 100644 --- a/src/solidlsp/language_servers/gopls.py +++ b/src/solidlsp/language_servers/gopls.py @@ -1,12 +1,15 @@ +import hashlib +import json import logging import os import pathlib import subprocess +from collections.abc import Hashable from typing import Any, cast from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -165,15 +168,13 @@ class Gopls(SolidLanguageServer): _CACHE_CONTEXT_ENV_KEYS = ("GOFLAGS", "GOOS", "GOARCH", "CGO_ENABLED") @override - def _document_symbols_cache_fingerprint(self) -> str | None: + def _document_symbols_cache_fingerprint(self) -> Hashable: """ Compute a deterministic fingerprint of the Go build context. The fingerprint includes gopls_settings and selected env vars that affect symbol discovery. """ - import hashlib - import json - + normalize_symbol_name_version = 1 gopls_settings_raw = self._custom_settings.settings.get("gopls_settings") gopls_settings: dict | None @@ -190,11 +191,14 @@ class Gopls(SolidLanguageServer): if value: env_subset[key] = value - # Return None only when BOTH settings and env subset are effectively empty. + # Version processed symbols even when the build context itself is empty. if gopls_settings is None and not env_subset: - return None + return normalize_symbol_name_version - fingerprint_data: dict[str, object] = {"env": env_subset} + fingerprint_data: dict[str, object] = { + "env": env_subset, + "normalize_symbol_name_version": normalize_symbol_name_version, + } if gopls_settings is not None: fingerprint_data["gopls_settings"] = gopls_settings @@ -202,6 +206,10 @@ class Gopls(SolidLanguageServer): return hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()[:16] + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + return symbol["name"].rsplit(".", 1)[-1] + def _start_server(self) -> None: """Start gopls server process""" diff --git a/src/solidlsp/language_servers/lua_ls.py b/src/solidlsp/language_servers/lua_ls.py index f8a37b3f..ef5e8674 100644 --- a/src/solidlsp/language_servers/lua_ls.py +++ b/src/solidlsp/language_servers/lua_ls.py @@ -11,12 +11,14 @@ import os import pathlib import platform import shutil +from collections.abc import Hashable from pathlib import Path from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -169,6 +171,26 @@ class LuaLanguageServer(SolidLanguageServer): ) self.request_id = 0 + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + original_name = symbol["name"] + + if symbol.get("kind") not in (SymbolKind.Function, SymbolKind.Method): + return original_name + + if "." in original_name: + return original_name.rsplit(".", 1)[-1] + + if ":" in original_name: + return original_name.rsplit(":", 1)[-1] + + return original_name + @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ diff --git a/src/solidlsp/language_servers/powershell_language_server.py b/src/solidlsp/language_servers/powershell_language_server.py index 828babd9..72f9ac27 100644 --- a/src/solidlsp/language_servers/powershell_language_server.py +++ b/src/solidlsp/language_servers/powershell_language_server.py @@ -14,12 +14,14 @@ import platform import shutil import tempfile import threading +from collections.abc import Hashable from pathlib import Path from overrides import override -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import FileUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo @@ -199,6 +201,30 @@ class PowerShellLanguageServer(SolidLanguageServer): ) self.server_ready = threading.Event() + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + original_name = symbol["name"] + symbol_kind = symbol.get("kind") + + # normalize class declarations + if symbol_kind == SymbolKind.Class: + return original_name.removeprefix("class ").split("{", 1)[0].strip() + + # normalize method signatures + if symbol_kind == SymbolKind.Method: + return original_name.split("(", 1)[0].rsplit(None, 1)[-1] + + # normalize function declarations + if symbol_kind == SymbolKind.Function: + return original_name.removeprefix("function ").split("(", 1)[0].strip() + + return original_name + @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ diff --git a/src/solidlsp/language_servers/sourcekit_lsp.py b/src/solidlsp/language_servers/sourcekit_lsp.py index ba55e35b..8d30daeb 100644 --- a/src/solidlsp/language_servers/sourcekit_lsp.py +++ b/src/solidlsp/language_servers/sourcekit_lsp.py @@ -3,12 +3,14 @@ import os import pathlib import subprocess import time +from collections.abc import Hashable from overrides import override from solidlsp import ls_types -from solidlsp.ls import SolidLanguageServer +from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_types import SymbolKind from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo from solidlsp.settings import SolidLSPSettings @@ -56,6 +58,23 @@ class SourceKitLSP(SolidLanguageServer): self._did_sleep_before_requesting_references = False self._initialization_timestamp: float | None = None + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + normalize_symbol_name_version = 1 + return normalize_symbol_name_version + + @override + def _normalize_symbol_name(self, symbol: RawDocumentSymbol, relative_file_path: str) -> str: + original_name = symbol["name"] + + if symbol.get("kind") not in (SymbolKind.Function, SymbolKind.Method, SymbolKind.Constructor): + return original_name + + if "(" not in original_name: + return original_name + + return original_name.split("(", 1)[0].strip() + @staticmethod def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: """ diff --git a/test/conftest.py b/test/conftest.py index f8981251..ee476321 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -41,6 +41,7 @@ _LANGUAGE_REPO_ALIASES: dict[Language, Language] = { Language.PHP_PHPACTOR: Language.PHP, Language.PYTHON_JEDI: Language.PYTHON, Language.RUBY_SOLARGRAPH: Language.RUBY, + Language.PYTHON_TY: Language.PYTHON, } diff --git a/test/resources/repos/go/test_repo/main.go b/test/resources/repos/go/test_repo/main.go index b38c31ff..a5716358 100644 --- a/test/resources/repos/go/test_repo/main.go +++ b/test/resources/repos/go/test_repo/main.go @@ -15,6 +15,10 @@ type DemoStruct struct { Field int } +func (d *DemoStruct) Value() int { + return d.Field +} + func UsingHelper() { Helper() } diff --git a/test/resources/repos/powershell/test_repo/main.ps1 b/test/resources/repos/powershell/test_repo/main.ps1 index 6829fda4..f6d331f7 100644 --- a/test/resources/repos/powershell/test_repo/main.ps1 +++ b/test/resources/repos/powershell/test_repo/main.ps1 @@ -105,5 +105,11 @@ function Main { Write-Output "Script completed successfully" } +class PersonFormatter { + [string] FormatName([string]$Name) { + return "Formatted: $Name" + } +} + # Run main function Main @args diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 0b40ab28..e2ca7362 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -187,7 +187,7 @@ class TestSerenaAgent: pytest.param(Language.PHP, "helperFunction", "Function", "helper.php", marks=pytest.mark.php), pytest.param(Language.CLOJURE, "greet", "Function", clj.CORE_PATH, marks=pytest.mark.clojure), pytest.param(Language.CSHARP, "Calculator", "Class", "Program.cs", marks=pytest.mark.csharp), - pytest.param(Language.POWERSHELL, "function Greet-User ()", "Function", "main.ps1", marks=pytest.mark.powershell), + pytest.param(Language.POWERSHELL, "Greet-User", "Function", "main.ps1", marks=pytest.mark.powershell), pytest.param(Language.CPP_CCLS, "add", "Function", "b.cpp", marks=pytest.mark.cpp), pytest.param(Language.LEAN4, "add", "Method", "Helper.lean", marks=pytest.mark.lean4), ], @@ -288,7 +288,7 @@ class TestSerenaAgent: marks=pytest.mark.clojure, ), pytest.param(Language.CSHARP, "Calculator", "Program.cs", "Program.cs", marks=pytest.mark.csharp), - pytest.param(Language.POWERSHELL, "function Greet-User ()", "main.ps1", "main.ps1", marks=pytest.mark.powershell), + pytest.param(Language.POWERSHELL, "Greet-User", "main.ps1", "main.ps1", marks=pytest.mark.powershell), pytest.param(Language.CPP_CCLS, "add", "b.cpp", "a.cpp", marks=pytest.mark.cpp), pytest.param(Language.LEAN4, "add", "Helper.lean", "Main.lean", marks=pytest.mark.lean4), ], diff --git a/test/serena/test_symbol.py b/test/serena/test_symbol.py index 60b936da..08188f49 100644 --- a/test/serena/test_symbol.py +++ b/test/serena/test_symbol.py @@ -5,7 +5,7 @@ import pytest from serena.jetbrains.jetbrains_types import SymbolDTO, SymbolDTOKey from serena.project import Project from serena.symbol import LanguageServerSymbol, LanguageServerSymbolRetriever, NamePathComponent, NamePathMatcher -from solidlsp.ls_config import Language +from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES class TestSymbolNameMatching: @@ -235,7 +235,7 @@ class TestSymbolNameMatching: @pytest.mark.python class TestLanguageServerSymbolRetriever: - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_info(self, project_with_ls: Project): symbol_retriever = LanguageServerSymbolRetriever(project_with_ls) create_user_method_symbol = symbol_retriever.find("UserService/create_user", within_relative_path="test_repo/services.py")[0] @@ -280,7 +280,7 @@ def _make_mock_symbols(count: int, *, relative_path: str = "test_repo/services.p class TestHoverBudget: """Tests for symbol_info_budget time budget behavior.""" - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_budget_not_exceeded_all_lookups_performed(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With a large budget, all hover lookups are performed.""" # Create symbol retriever with a mock agent that has large budget @@ -309,7 +309,7 @@ class TestHoverBudget: assert all(info is not None for info in result.values()) assert len(result) == 3 - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_budget_exceeded_partial_info(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With a small budget, hover lookups stop and remaining symbols get None info.""" project_with_ls.serena_config.symbol_info_budget = 0.1 @@ -353,7 +353,7 @@ class TestHoverBudget: assert result_list[3] is None assert result_list[4] is None - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_budget_zero_means_unlimited(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """With budget=0, all hover lookups proceed (no early stopping).""" project_with_ls.serena_config.symbol_info_budget = 0.0 @@ -380,7 +380,7 @@ class TestHoverBudget: assert call_count == 5 assert all(info is not None for info in result.values()) - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_project_budget_overrides_global(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """Project-level budget overrides global budget.""" # Create symbol retriever with global budget 10.0 but project budget 0.05 @@ -418,7 +418,7 @@ class TestHoverBudget: # So 2 calls succeed (proving project budget 0.05 overrode global 10.0) assert call_count == 2 - @pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project_with_ls", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_project_null_inherits_global(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch): """When project budget is None, global budget is used.""" # Create symbol retriever with project budget=None (inherit global) diff --git a/test/solidlsp/al/test_al_basic.py b/test/solidlsp/al/test_al_basic.py index c226421f..a52e2f14 100644 --- a/test/solidlsp/al/test_al_basic.py +++ b/test/solidlsp/al/test_al_basic.py @@ -6,8 +6,10 @@ from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer from solidlsp.language_servers.al_language_server import ALLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils from test.conftest import language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols pytestmark = [pytest.mark.al, pytest.mark.skipif(not language_tests_enabled(Language.AL), reason="AL tests are disabled")] @@ -556,3 +558,38 @@ class TestALPathNormalization: f"Hover for {symbol_name} should have injection with mixed paths. Got: {value[:200]}" ) break + + @pytest.mark.parametrize("language_server", [Language.AL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + is_al_display_symbol = ( + s["name"].endswith((".Codeunit", ".Page", ".Table", ".TableExt", ".Enum", ".Interface")) + or (s["name"].startswith('"') and s["name"].endswith('"')) + or s["name"].startswith( + ( + "Enum Name ", + "Area ", + "Group ", + "Field ", + "Part ", + "SystemPart ", + "Repeater ", + "ActionRef ", + "Key ", + "FieldGroup ", + ) + ) + or ":" in s["name"] + ) + if not is_al_display_symbol and has_malformed_name( + s, + whitespace_allowed=s["kind"] in {SymbolKind.Class, SymbolKind.Struct, SymbolKind.Interface, SymbolKind.Enum}, + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/ansible/test_ansible_basic.py b/test/solidlsp/ansible/test_ansible_basic.py index 154b2231..bf6dc6ca 100644 --- a/test/solidlsp/ansible/test_ansible_basic.py +++ b/test/solidlsp/ansible/test_ansible_basic.py @@ -13,6 +13,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.skipif(platform.system() == "Windows", reason="ansible-language-server does not support native Windows operation") @@ -51,3 +52,17 @@ class TestAnsibleLanguageServerBasics: assert len(result) > 0, "Expected non-empty completion list" labels = [item["completionText"] for item in result if "completionText" in item] assert labels, f"Expected completions with completionText, got: {result[:3]}" + + @pytest.mark.xfail(reason="Seems like ansible LS lacks basic functionality at the moment, textDocument/documentSymbol doesn't work") + @pytest.mark.parametrize("language_server", [Language.ANSIBLE], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/bash/test_bash_basic.py b/test/solidlsp/bash/test_bash_basic.py index a3ead08f..c7715a29 100644 --- a/test/solidlsp/bash/test_bash_basic.py +++ b/test/solidlsp/bash/test_bash_basic.py @@ -9,6 +9,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.bash @@ -119,3 +120,16 @@ class TestBashLanguageServerBasics: # Verify total counts match expectations assert len(main_functions) >= 3, f"Should find at least 3 functions in main.sh, found {len(main_functions)}" assert len(utils_functions) >= 8, f"Should find at least 8 functions in utils.sh, found {len(utils_functions)}" + + @pytest.mark.parametrize("language_server", [Language.BASH], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/clojure/test_clojure_basic.py b/test/solidlsp/clojure/test_clojure_basic.py index 1a199673..c42067ac 100644 --- a/test/solidlsp/clojure/test_clojure_basic.py +++ b/test/solidlsp/clojure/test_clojure_basic.py @@ -3,8 +3,9 @@ import pytest from serena.project import Project from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language -from solidlsp.ls_types import UnifiedSymbolInformation +from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation from test.conftest import language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols from . import CORE_PATH, UTILS_PATH @@ -187,6 +188,8 @@ class TestLanguageServerBasics: assert found_relevant_references, f"Should have found calculate-area referencing multiply, but got: {result}" +@pytest.mark.skipif(not language_tests_enabled(Language.CLOJURE), reason="Clojure tests are disabled") +@pytest.mark.clojure class TestProjectBasics: @pytest.mark.parametrize("project", [Language.CLOJURE], indirect=True) def test_retrieve_content_around_line(self, project: Project): @@ -221,3 +224,20 @@ class TestProjectBasics: assert result is not None, "Should find require statements" utils_matches = [match for match in result if match.source_file_path and "utils.clj" in match.source_file_path] assert len(utils_matches) > 0, "Should find require statement in utils.clj" + + @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + # clojure-lsp exposes namespace and dependency/container entries in addition to real vars/functions. + # Those qualified names are not the target of this regression check. + if s["kind"] in {SymbolKind.Namespace, SymbolKind.Struct}: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/conftest.py b/test/solidlsp/conftest.py new file mode 100644 index 00000000..08bb6a01 --- /dev/null +++ b/test/solidlsp/conftest.py @@ -0,0 +1,56 @@ +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation + +PYTHON_BACKEND_LANGUAGES = [Language.PYTHON, Language.PYTHON_TY] + + +def has_malformed_name( + symbol: UnifiedSymbolInformation, + whitespace_allowed: bool = False, + period_allowed: bool = False, + colon_allowed: bool = False, + brace_allowed: bool = False, + parenthesis_allowed: bool = False, + comma_allowed: bool = False, +) -> bool: + forbidden_chars: list[str] = [] + + if not whitespace_allowed: + forbidden_chars.append(" ") + if not period_allowed: + forbidden_chars.append(".") + if not colon_allowed: + forbidden_chars.append(":") + if not brace_allowed: + forbidden_chars.append("{") + if not parenthesis_allowed: + forbidden_chars.append("(") + if not comma_allowed: + forbidden_chars.append(",") + + return any(separator in symbol["name"] for separator in forbidden_chars) + + +def request_all_symbols(language_server: SolidLanguageServer) -> list[UnifiedSymbolInformation]: + result: list[UnifiedSymbolInformation] = [] + + def visit(symbol: UnifiedSymbolInformation) -> None: + result.append(symbol) + for child in symbol.get("children", []): + visit(child) + + symbols = language_server.request_full_symbol_tree() + for symbol in symbols: + visit(symbol) + + return result + + +def format_symbol_for_assert(symbol: UnifiedSymbolInformation) -> str: + relative_path = symbol.get("location", {}).get("relativePath", "") + try: + kind = SymbolKind(symbol["kind"]).name + except ValueError: + kind = str(symbol["kind"]) + return f"{symbol['name']} [{kind}] ({relative_path})" diff --git a/test/solidlsp/cpp/test_cpp_basic.py b/test/solidlsp/cpp/test_cpp_basic.py index a8767949..3ca52693 100644 --- a/test/solidlsp/cpp/test_cpp_basic.py +++ b/test/solidlsp/cpp/test_cpp_basic.py @@ -15,6 +15,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols def _ccls_available() -> bool: @@ -142,3 +143,16 @@ int use_add() { # Clean up the new file if os.path.exists(new_file_abs_path): os.remove(new_file_abs_path) + + @pytest.mark.parametrize("language_server", _cpp_servers, indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/csharp/test_csharp_basic.py b/test/solidlsp/csharp/test_csharp_basic.py index 3d538061..91f67781 100644 --- a/test/solidlsp/csharp/test_csharp_basic.py +++ b/test/solidlsp/csharp/test_csharp_basic.py @@ -15,8 +15,10 @@ from solidlsp.language_servers.csharp_language_server import ( find_solution_or_project_file, ) from solidlsp.ls_config import Language, LanguageServerConfig +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils from solidlsp.settings import SolidLSPSettings +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.csharp @@ -359,3 +361,16 @@ class TestCSharpSolutionProjectOpening: # Verify the file actually exists assert os.path.exists(result) + + @pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, period_allowed=s["kind"] == SymbolKind.Namespace): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/dart/test_dart_basic.py b/test/solidlsp/dart/test_dart_basic.py index cdd98a97..0a5b69cf 100644 --- a/test/solidlsp/dart/test_dart_basic.py +++ b/test/solidlsp/dart/test_dart_basic.py @@ -8,6 +8,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.dart @@ -361,3 +362,16 @@ class TestDartLanguageServer: if main_dart_refs: for ref in main_dart_refs: assert "range" in ref or "location" in ref + + @pytest.mark.parametrize("language_server", [Language.DART], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/elixir/test_elixir_basic.py b/test/solidlsp/elixir/test_elixir_basic.py index fe18cd34..11b983ca 100644 --- a/test/solidlsp/elixir/test_elixir_basic.py +++ b/test/solidlsp/elixir/test_elixir_basic.py @@ -11,6 +11,8 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON @@ -110,3 +112,29 @@ class TestElixirBasic: for _ in range(3): symbols = language_server.request_document_symbols("lib/services.ex").get_all_symbols_and_roots() assert symbols is not None + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] in {SymbolKind.Module, SymbolKind.Namespace, SymbolKind.Struct}: + continue + + allow_test_style_name = s["name"].startswith(('test "', 'describe "')) + allow_struct_name = s["name"].startswith("%") + allow_type_name = s["name"].startswith("@type ") + if has_malformed_name( + s, + whitespace_allowed=allow_test_style_name or allow_type_name, + period_allowed=allow_struct_name, + colon_allowed=allow_test_style_name, + brace_allowed=allow_test_style_name or allow_struct_name, + comma_allowed=allow_test_style_name, + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/elm/test_elm_basic.py b/test/solidlsp/elm/test_elm_basic.py index 6283d914..5ff6fe22 100644 --- a/test/solidlsp/elm/test_elm_basic.py +++ b/test/solidlsp/elm/test_elm_basic.py @@ -1,12 +1,16 @@ import os +import shutil import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("elm") is None and not is_ci, reason="Elm compiler is not available") @pytest.mark.elm class TestElmLanguageServer: @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True) @@ -52,3 +56,16 @@ class TestElmLanguageServer: # Verify that at least one reference is in Main.elm (where formatMessage is used) assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Expected to find usage of formatMessage in Main.elm" + + @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/erlang/test_erlang_basic.py b/test/solidlsp/erlang/test_erlang_basic.py index 50ab3197..a1dba06a 100644 --- a/test/solidlsp/erlang/test_erlang_basic.py +++ b/test/solidlsp/erlang/test_erlang_basic.py @@ -9,6 +9,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols from . import ERLANG_LS_UNAVAILABLE, ERLANG_LS_UNAVAILABLE_REASON @@ -41,3 +42,16 @@ class TestErlangLanguageServerBasics: pytest.skip("Erlang language server not fully initialized") else: raise + + @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/fortran/test_fortran_basic.py b/test/solidlsp/fortran/test_fortran_basic.py index 1554e959..e7a45235 100644 --- a/test/solidlsp/fortran/test_fortran_basic.py +++ b/test/solidlsp/fortran/test_fortran_basic.py @@ -11,6 +11,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Mark all tests in this module as fortran tests pytestmark = pytest.mark.fortran @@ -272,3 +273,16 @@ class TestFortranLanguageServer: _refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) # refs might be empty if Point3D isn't used elsewhere, but the call should not fail # The important thing is that it doesn't error due to wrong character position + + @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/fsharp/test_fsharp_basic.py b/test/solidlsp/fsharp/test_fsharp_basic.py index 8e6ff221..d0f52443 100644 --- a/test/solidlsp/fsharp/test_fsharp_basic.py +++ b/test/solidlsp/fsharp/test_fsharp_basic.py @@ -8,6 +8,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Currently, most F# tests fail, there seems to be a regression or instability. @@ -183,3 +184,16 @@ class TestFSharpLanguageServer: # This is a successful test - FsAutoComplete is working with F# files assert True, "F# language server can handle files successfully" + + @pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/go/test_go_basic.py b/test/solidlsp/go/test_go_basic.py index 8d83298c..93fcd98e 100644 --- a/test/solidlsp/go/test_go_basic.py +++ b/test/solidlsp/go/test_go_basic.py @@ -3,9 +3,11 @@ from pathlib import Path import pytest +from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.go @@ -17,6 +19,20 @@ class TestGoLanguageServer: assert SymbolUtils.symbol_tree_contains_name(symbols, "Helper"), "Helper function not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoStruct"), "DemoStruct not found in symbol tree" + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) + def test_find_symbol_matches_go_method_by_bare_name(self, language_server: SolidLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree(within_relative_path="main.go") + + assert SymbolUtils.symbol_tree_contains_name(symbols, "Value"), "Expected Go method name to be normalized to bare name" + assert not SymbolUtils.symbol_tree_contains_name(symbols, "(*DemoStruct).Value"), ( + "Expected receiver-qualified Go method name to be normalized away" + ) + + bare_name_matches = [match for root in symbols for match in LanguageServerSymbol(root).find("Value")] + + assert bare_name_matches, "Expected a Go method to match by bare name" + assert all(match.name == "Value" for match in bare_name_matches) + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None: file_path = os.path.join("main.go") @@ -192,3 +208,16 @@ class TestGoBuildTags: # A cache miss should repopulate and mark caches modified. _assert_caches_modified(ls_foo) + + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/groovy/test_groovy_basic.py b/test/solidlsp/groovy/test_groovy_basic.py index 57e0baf3..47f22ed8 100644 --- a/test/solidlsp/groovy/test_groovy_basic.py +++ b/test/solidlsp/groovy/test_groovy_basic.py @@ -8,6 +8,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import SymbolUtils from solidlsp.settings import SolidLSPSettings +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.groovy @@ -103,3 +104,16 @@ class TestGroovyLanguageServer: assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "ModelUser"), "ModelUser missing from overview" + + def test_bare_symbol_names(self) -> None: + assert self.language_server is not None + all_symbols = request_all_symbols(self.language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/haskell/test_haskell_basic.py b/test/solidlsp/haskell/test_haskell_basic.py index b080a306..67452151 100644 --- a/test/solidlsp/haskell/test_haskell_basic.py +++ b/test/solidlsp/haskell/test_haskell_basic.py @@ -19,6 +19,8 @@ import pytest from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.haskell @@ -217,3 +219,24 @@ class TestHaskellLanguageServer: assert any("Main.hs" in path or "Calculator.hs" in path for path in calc_ref_paths), ( f"Expected Calculator to be referenced in Main.hs or Calculator.hs, got: {calc_ref_paths}" ) + + @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] == SymbolKind.Module: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + diagnostics = [ + { + "formatted": format_symbol_for_assert(sym), + "name": sym["name"], + "kind": SymbolKind(sym["kind"]).name, + "detail": sym.get("detail"), + } + for sym in malformed_symbols + ] + pytest.fail(f"Found malformed symbols: {diagnostics}", pytrace=False) diff --git a/test/solidlsp/hlsl/test_hlsl_basic.py b/test/solidlsp/hlsl/test_hlsl_basic.py index 7034579d..90d1c864 100644 --- a/test/solidlsp/hlsl/test_hlsl_basic.py +++ b/test/solidlsp/hlsl/test_hlsl_basic.py @@ -12,7 +12,9 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_exceptions import SolidLSPException +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols def _find_symbol_by_name(language_server: SolidLanguageServer, file_path: str, name: str) -> dict[str, Any] | None: @@ -158,3 +160,16 @@ class TestHlslHover: hover_info = language_server.request_hover("common.hlsl", 3, 7) assert hover_info is not None, "Hover should return information for VertexInput" assert "contents" in hover_info, "Hover should have contents" + + @pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, period_allowed=s["kind"] == SymbolKind.File): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/java/test_java_basic.py b/test/solidlsp/java/test_java_basic.py index 0810dac2..56b3bbf4 100644 --- a/test/solidlsp/java/test_java_basic.py +++ b/test/solidlsp/java/test_java_basic.py @@ -6,6 +6,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils from test.conftest import language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols pytestmark = [pytest.mark.java, pytest.mark.skipif(not language_tests_enabled(Language.JAVA), reason="Java tests disabled")] @@ -50,3 +51,16 @@ class TestJavaLanguageServer: assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview" + + @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/julia/test_julia_basic.py b/test/solidlsp/julia/test_julia_basic.py index 99bf720c..44adb43d 100644 --- a/test/solidlsp/julia/test_julia_basic.py +++ b/test/solidlsp/julia/test_julia_basic.py @@ -1,9 +1,14 @@ +import shutil + import pytest from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("julia") is None and not is_ci, reason="Julia is not available") @pytest.mark.julia class TestJuliaLanguageServer: @pytest.mark.parametrize("language_server", [Language.JULIA], indirect=True) @@ -48,3 +53,16 @@ class TestJuliaLanguageServer: reference_paths = [ref["relativePath"] for ref in references] # The reference might be in either file (definition or usage) assert "main.jl" in reference_paths or "lib/helper.jl" in reference_paths + + @pytest.mark.parametrize("language_server", [Language.JULIA], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/kotlin/test_kotlin_basic.py b/test/solidlsp/kotlin/test_kotlin_basic.py index 762c391c..9a990458 100644 --- a/test/solidlsp/kotlin/test_kotlin_basic.py +++ b/test/solidlsp/kotlin/test_kotlin_basic.py @@ -4,8 +4,10 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Kotlin LSP (IntelliJ-based, pre-alpha v261) crashes on JVM restart under CI resource constraints @@ -55,3 +57,16 @@ class TestKotlinLanguageServer: assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview" + + @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, period_allowed=s["kind"] == SymbolKind.File): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/lua/test_lua_basic.py b/test/solidlsp/lua/test_lua_basic.py index ab58ec5e..f140881b 100644 --- a/test/solidlsp/lua/test_lua_basic.py +++ b/test/solidlsp/lua/test_lua_basic.py @@ -10,6 +10,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.lua @@ -253,3 +254,21 @@ class TestLuaLanguageServer: # The test file should have some content that references calculator symbol_list = test_symbols[0] if isinstance(test_symbols, tuple) else test_symbols assert len(symbol_list) > 0, "test_calculator.lua should have symbols" + + @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name( + s, + whitespace_allowed=s["name"] == " ", + period_allowed="." in s["name"], + colon_allowed=":" in s["name"], + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/luau/test_luau_basic.py b/test/solidlsp/luau/test_luau_basic.py index 264ace92..4564e257 100644 --- a/test/solidlsp/luau/test_luau_basic.py +++ b/test/solidlsp/luau/test_luau_basic.py @@ -9,6 +9,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.luau @@ -151,3 +152,16 @@ class TestLuauLanguageServer: assert definition["uri"].endswith("init.luau"), f"Definition should be in init.luau, got: {definition['uri']}" # createConfig is defined at line 8 (0-indexed): `local function createConfig(...)` assert definition["range"]["start"]["line"] == 8, f"Definition should be at line 8, got line {definition['range']['start']['line']}" + + @pytest.mark.parametrize("language_server", [Language.LUAU], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/markdown/test_markdown_basic.py b/test/solidlsp/markdown/test_markdown_basic.py index 5a3c65f8..34f57533 100644 --- a/test/solidlsp/markdown/test_markdown_basic.py +++ b/test/solidlsp/markdown/test_markdown_basic.py @@ -11,6 +11,7 @@ from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.markdown @@ -93,3 +94,16 @@ class TestMarkdownLanguageServerBasics: for symbol in all_symbols: assert symbol["kind"] == SymbolKind.Namespace, f"Nested heading '{symbol['name']}' should be remapped to Namespace" + + @pytest.mark.parametrize("language_server", [Language.MARKDOWN], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, whitespace_allowed=True, parenthesis_allowed=True): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/matlab/test_matlab_basic.py b/test/solidlsp/matlab/test_matlab_basic.py index a6c423c8..71724ff7 100644 --- a/test/solidlsp/matlab/test_matlab_basic.py +++ b/test/solidlsp/matlab/test_matlab_basic.py @@ -16,6 +16,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Skip all tests if MATLAB is not available pytestmark = pytest.mark.matlab @@ -115,3 +116,16 @@ class TestMatlabLanguageServerReferences: # Should find references in both main.m and Calculator.m assert references is not None + + @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/nix/test_nix_basic.py b/test/solidlsp/nix/test_nix_basic.py index 42d4d414..23f4311a 100644 --- a/test/solidlsp/nix/test_nix_basic.py +++ b/test/solidlsp/nix/test_nix_basic.py @@ -10,7 +10,9 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Skip all Nix tests on Windows as Nix doesn't support Windows pytestmark = pytest.mark.skipif(platform.system() == "Windows", reason="Nix and nil are not available on Windows") @@ -218,3 +220,33 @@ class TestNixLanguageServer: root = symbols[0] assert isinstance(root, dict), "Root should be a dict" assert "name" in root, "Root should have a name" + + @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + # nixd can surface anonymous expression nodes using synthetic display labels + # like "(anonymous lambda)", "(dynamic string)", "{anonymous}", and + # "(dynamic attribute name)". It also emits literal value nodes such as + # strings and arrays. These do not correspond to named declarations, so + # there is no bare identifier we could or should normalize them to in the + # Nix wrapper. This test is only meant to enforce bare names for symbols + # that actually have declaration names. + if s["kind"] in {SymbolKind.String, SymbolKind.Array}: + continue + if s["name"] in {"(anonymous lambda)", "(dynamic string)", "{anonymous}", "(dynamic attribute name)"}: + continue + if s["kind"] in {SymbolKind.Field, SymbolKind.Property} and "." in s["name"]: + continue + if has_malformed_name( + s, + whitespace_allowed=s["name"] == "(anonymous lambda)", + parenthesis_allowed=s["name"] == "(anonymous lambda)", + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/ocaml/test_ocaml_basic.py b/test/solidlsp/ocaml/test_ocaml_basic.py index 82af5fa9..f3dca2eb 100644 --- a/test/solidlsp/ocaml/test_ocaml_basic.py +++ b/test/solidlsp/ocaml/test_ocaml_basic.py @@ -1,12 +1,16 @@ import os +import shutil import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("opam") is None and not is_ci, reason="OPAM is not available") @pytest.mark.ocaml class TestOCamlLanguageServer: @pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True) @@ -49,8 +53,6 @@ class TestOCamlLanguageServer: def test_reason_file_patterns(self) -> None: """Test that OCaml language configuration recognizes Reason file extensions""" - from solidlsp.ls_config import Language - ocaml_lang = Language.OCAML file_matcher = ocaml_lang.get_source_fn_matcher() @@ -150,3 +152,19 @@ class TestOCamlLanguageServer: file_path = os.path.join("bin", "main.ml") symbols, _roots = language_server.request_document_symbols(file_path).get_all_symbols_and_roots() assert len(symbols) > 0, "Should find symbols in main.ml that use opened modules" + + @pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + # happens for lambdas a la let () = ... + if s["name"] == "()": + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/pascal/test_pascal_basic.py b/test/solidlsp/pascal/test_pascal_basic.py index d926207a..e9636e94 100644 --- a/test/solidlsp/pascal/test_pascal_basic.py +++ b/test/solidlsp/pascal/test_pascal_basic.py @@ -10,12 +10,15 @@ Uses genericptr/pascal-language-server which returns SymbolInformation[] format: - Method names don't include parent class prefix; uses containerName instead """ +import shutil + import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind -from test.conftest import language_tests_enabled +from test.conftest import is_ci, language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols pytestmark = [ pytest.mark.pascal, @@ -24,6 +27,7 @@ pytestmark = [ @pytest.mark.pascal +@pytest.mark.skipif(shutil.which("fpc") is None and not is_ci, reason="Free Pascal compiler is not available") class TestPascalLanguageServerBasics: """Test basic functionality of the Pascal language server.""" @@ -193,3 +197,18 @@ class TestPascalLanguageServerBasics: # Should contain the doc comment assert "Calculates the sum" in value, f"Hover should include doc comment. Got: {value[:500]}" + + @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] == SymbolKind.Package: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/perl/test_perl_basic.py b/test/solidlsp/perl/test_perl_basic.py index 59dbcc5f..41682e3f 100644 --- a/test/solidlsp/perl/test_perl_basic.py +++ b/test/solidlsp/perl/test_perl_basic.py @@ -5,6 +5,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.perl @@ -74,3 +75,16 @@ class TestPerlLanguageServer: main_pl_lines = sorted([ref["range"]["start"]["line"] for ref in main_pl_refs]) assert 17 in main_pl_lines, f"Expected reference at line 18 (0-indexed 17), found: {main_pl_lines}" assert 20 in main_pl_lines, f"Expected reference at line 21 (0-indexed 20), found: {main_pl_lines}" + + @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/php/test_php_basic.py b/test/solidlsp/php/test_php_basic.py index bc59d471..aab824f3 100644 --- a/test/solidlsp/php/test_php_basic.py +++ b/test/solidlsp/php/test_php_basic.py @@ -1,3 +1,4 @@ +import subprocess from pathlib import Path import pytest @@ -5,10 +6,18 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import is_ci, is_windows, language_tests_enabled +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols + + +def _php_supports_phpactor() -> bool: + completed_process = subprocess.run(["php", "-m"], capture_output=True, text=True, check=False) + installed_extensions = {line.strip().lower() for line in completed_process.stdout.splitlines()} + return "openssl" in installed_extensions + _php_servers: list[Language] = [Language.PHP] if language_tests_enabled(Language.PHP_PHPACTOR): - if not (is_windows and is_ci): # TODO: Phpactor tests are flaky in Windows CI and can even cause hangs #1040 + if not (is_windows and is_ci) and _php_supports_phpactor(): _php_servers.append(Language.PHP_PHPACTOR) @@ -256,3 +265,16 @@ class TestPhpLanguageServers: dog_root = next((s for s in symbols if s.get("name") == "Dog"), None) if dog_root is not None: assert SymbolUtils.symbol_tree_contains_name([dog_root], "greet"), "greet should be nested under Dog in symbol tree" + + @pytest.mark.parametrize("language_server", _php_servers, indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/powershell/test_powershell_basic.py b/test/solidlsp/powershell/test_powershell_basic.py index 3d6ef7a1..bf1b9e46 100644 --- a/test/solidlsp/powershell/test_powershell_basic.py +++ b/test/solidlsp/powershell/test_powershell_basic.py @@ -9,6 +9,8 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.powershell @@ -130,6 +132,14 @@ class TestPowerShellLanguageServerBasics: assert len(main_functions) >= 3, f"Should find at least 3 functions in main.ps1, found {len(main_functions)}" assert len(utils_functions) >= 8, f"Should find at least 8 functions in utils.ps1, found {len(utils_functions)}" + @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True) + def test_powershell_class_method_symbols_use_bare_method_name(self, language_server: SolidLanguageServer) -> None: + """Test whether PSES already reports class methods with bare names.""" + symbols = language_server.request_full_symbol_tree(within_relative_path="main.ps1") + + assert SymbolUtils.symbol_tree_contains_name(symbols, "PersonFormatter"), "Should find PersonFormatter class in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "FormatName"), "Expected PowerShell method to be exposed with bare name" + @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True) def test_powershell_find_references_within_file(self, language_server: SolidLanguageServer) -> None: """Test finding references to a function within the same file.""" @@ -172,3 +182,16 @@ class TestPowerShellLanguageServerBasics: assert any("utils.ps1" in loc.get("uri", "") for loc in definition_locations), ( f"Should find definition in utils.ps1, got {definition_locations}" ) + + @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, colon_allowed=s["name"].startswith("$")): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/python/test_python_basic.py b/test/solidlsp/python/test_python_basic.py index faeca67a..f57b37bd 100644 --- a/test/solidlsp/python/test_python_basic.py +++ b/test/solidlsp/python/test_python_basic.py @@ -12,14 +12,14 @@ import pytest from serena.project import Project from serena.util.text_utils import LineType from solidlsp import SolidLanguageServer -from solidlsp.ls_config import Language +from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES, format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.python class TestPythonLanguageServerBasics: """Test basic functionality of the language server.""" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_references_user_class(self, language_server: SolidLanguageServer) -> None: """Test request_references on the User class.""" # Get references to the User class in models.py @@ -34,7 +34,7 @@ class TestPythonLanguageServerBasics: references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert len(references) > 1, "User class should be referenced in multiple files (using selectionRange if present)" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_references_item_class(self, language_server: SolidLanguageServer) -> None: """Test request_references on the Item class.""" # Get references to the Item class in models.py @@ -50,7 +50,7 @@ class TestPythonLanguageServerBasics: services_references = [ref for ref in references if "services.py" in ref["uri"]] assert len(services_references) > 0, "At least one reference should be in services.py (using selectionRange if present)" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_references_function_parameter(self, language_server: SolidLanguageServer) -> None: """Test request_references on a function parameter.""" # Get references to the id parameter in get_user method @@ -65,7 +65,7 @@ class TestPythonLanguageServerBasics: references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert len(references) > 0, "id parameter should be referenced within the method (using selectionRange if present)" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_references_create_user_method(self, language_server: SolidLanguageServer) -> None: # Get references to the create_user method in UserService file_path = os.path.join("test_repo", "services.py") @@ -81,7 +81,7 @@ class TestPythonLanguageServerBasics: class TestProjectBasics: - @pytest.mark.parametrize("project", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_retrieve_content_around_line(self, project: Project) -> None: """Test retrieve_content_around_line functionality with various scenarios.""" file_path = os.path.join("test_repo", "models.py") @@ -188,7 +188,7 @@ class TestProjectBasics: else: assert line.match_type == LineType.AFTER_MATCH - @pytest.mark.parametrize("project", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("project", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_search_files_for_pattern(self, project: Project) -> None: """Test search_files_for_pattern with various patterns and glob filters.""" # Test 1: Search for class definitions across all files @@ -233,3 +233,16 @@ class TestProjectBasics: no_match_pattern = r"def\s+this_method_does_not_exist\s*\([^)]*\):" matches = project.search_source_files_for_pattern(no_match_pattern) assert len(matches) == 0 + + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/python/test_retrieval_with_ignored_dirs.py b/test/solidlsp/python/test_retrieval_with_ignored_dirs.py index 031f185c..9c12af8d 100644 --- a/test/solidlsp/python/test_retrieval_with_ignored_dirs.py +++ b/test/solidlsp/python/test_retrieval_with_ignored_dirs.py @@ -6,6 +6,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import start_ls_context +from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES # This mark will be applied to all tests in this module pytestmark = pytest.mark.python @@ -19,7 +20,7 @@ def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]: yield ls -@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) +@pytest.mark.parametrize("ls_with_ignored_dirs", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that request_full_symbol_tree ignores the configured directory.""" root = ls_with_ignored_dirs.request_full_symbol_tree()[0] @@ -28,7 +29,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): assert children_names == {"test_repo", "examples"} -@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True) +@pytest.mark.parametrize("ls_with_ignored_dirs", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): """Tests that find_references ignores the configured directory.""" # Location of Item, which is referenced in scripts @@ -42,7 +43,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer): assert not any("scripts" in ref["relativePath"] for ref in references) -@pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True) +@pytest.mark.parametrize("repo_path", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" ignored_paths = ["*ipts", "custom_t*"] diff --git a/test/solidlsp/python/test_symbol_retrieval.py b/test/solidlsp/python/test_symbol_retrieval.py index ff98bd70..3baff0a0 100644 --- a/test/solidlsp/python/test_symbol_retrieval.py +++ b/test/solidlsp/python/test_symbol_retrieval.py @@ -12,8 +12,8 @@ import pytest from serena.symbol import LanguageServerSymbol from solidlsp import SolidLanguageServer -from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES pytestmark = pytest.mark.python @@ -21,7 +21,7 @@ pytestmark = pytest.mark.python class TestLanguageServerSymbols: """Test the language server's symbol-related functionality.""" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a function.""" # Test for a position inside the create_user method @@ -36,7 +36,7 @@ class TestLanguageServerSymbols: if "body" in containing_symbol: assert containing_symbol["body"].get_text().strip().startswith("def create_user(self") - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_references_to_variables(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a variable.""" file_path = os.path.join("test_repo", "variables.py") @@ -51,7 +51,7 @@ class TestLanguageServerSymbols: assert "dataclass_instance" in ref_names assert "second_dataclass" in ref_names - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a class.""" # Test for a position inside the UserService class but outside any method @@ -64,7 +64,7 @@ class TestLanguageServerSymbols: assert containing_symbol["name"] == "UserService" assert containing_symbol["kind"] == SymbolKind.Class - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol with nested scopes.""" # Test for a position inside a method which is inside a class @@ -90,7 +90,7 @@ class TestLanguageServerSymbols: assert parent_symbol["name"] == "UserService" assert parent_symbol["kind"] == SymbolKind.Class - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol for a position with no containing symbol.""" # Test for a position outside any function/class (e.g., in imports) @@ -101,7 +101,7 @@ class TestLanguageServerSymbols: # Should return None or an empty dictionary assert containing_symbol is None or containing_symbol == {} - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a function.""" # Test referencing symbols for create_user function @@ -125,7 +125,7 @@ class TestLanguageServerSymbols: assert "start" in symbol["location"]["range"] assert "end" in symbol["location"]["range"] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a class.""" # Test referencing symbols for User class @@ -146,7 +146,7 @@ class TestLanguageServerSymbols: ] assert len(services_references) > 0, "No referencing symbols from services.py for User (selectionRange)" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a function parameter.""" # Test referencing symbols for id parameter in get_user @@ -167,7 +167,7 @@ class TestLanguageServerSymbols: ] assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None: """Test request_referencing_symbols for a position with no symbol.""" # For positions with no symbol, the method might throw an error or return None/empty list @@ -185,7 +185,7 @@ class TestLanguageServerSymbols: pass # Tests for request_defining_symbol - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a variable usage.""" # Test finding the definition of a symbol in the create_user method @@ -204,7 +204,7 @@ class TestLanguageServerSymbols: if "location" in defining_symbol and "uri" in defining_symbol["location"]: assert "services.py" in defining_symbol["location"]["uri"] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for an imported class.""" # Test finding the definition of the 'User' class used in the UserService.create_user method @@ -216,7 +216,7 @@ class TestLanguageServerSymbols: assert defining_symbol is not None assert defining_symbol.get("name") == "User" - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a method call.""" # Create an example file path for a file that calls UserService.create_user @@ -241,7 +241,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify method call definition - file structure may differ from expected") - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a position with no symbol.""" # Test for a position with no symbol (e.g., whitespace or comment) @@ -252,7 +252,7 @@ class TestLanguageServerSymbols: # Should return None for positions with no symbol assert defining_symbol is None - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None: """Test request_containing_symbol where the symbol is a variable.""" # Test for a position inside a variable definition @@ -265,7 +265,7 @@ class TestLanguageServerSymbols: assert containing_symbol["name"] == "user_var_str" assert containing_symbol["kind"] == SymbolKind.Variable - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None: """Test request_defining_symbol for a nested function or closure.""" # Use the existing nested.py file which contains nested classes and methods @@ -290,15 +290,8 @@ class TestLanguageServerSymbols: # Test 3: Find definition of a method-local function defining_symbol = language_server.request_defining_symbol(file_path, 9, 15) # Position inside func_within_func - # This is challenging for many language servers and may fail - try: - assert defining_symbol is not None - assert defining_symbol.get("name") == "func_within_func" - except (AssertionError, TypeError, KeyError): - # This is expected to potentially fail in many implementations - import warnings - - warnings.warn("Could not resolve nested class method definition - implementation limitation") + assert defining_symbol is not None + assert defining_symbol.get("name") == "func_within_func" # Test 2: Find definition of the nested class defining_symbol = language_server.request_defining_symbol(file_path, 15, 18) # Position of NestedClass @@ -314,9 +307,9 @@ class TestLanguageServerSymbols: # This is challenging for many language servers and may fail assert defining_symbol is not None assert defining_symbol.get("name") == "func_within_func" - assert defining_symbol.get("kind") == SymbolKind.Function.value + assert defining_symbol.get("kind") in [SymbolKind.Function.value, SymbolKind.Method.value] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None: """Test the integration between different symbol-related methods.""" # This test demonstrates using the various symbol methods together @@ -365,7 +358,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify container hierarchy - implementation detail") - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file @@ -395,7 +388,7 @@ class TestLanguageServerSymbols: ).get_all_symbols_and_roots() assert user_management_roots == user_management_node["children"] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file @@ -418,7 +411,7 @@ class TestLanguageServerSymbols: ).get_all_symbols_and_roots() assert user_management_roots == user_management_node["children"] - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None: """Test that request_dir_overview returns correct symbol information for files in a directory.""" # Get overview of the examples directory @@ -442,7 +435,7 @@ class TestLanguageServerSymbols: retrieved_symbols = {symbol["name"] for symbol in services_symbols if "name" in symbol} assert expected_symbols.issubset(retrieved_symbols) - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) + @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True) def test_request_document_overview(self, language_server: SolidLanguageServer) -> None: """Test that request_document_overview returns correct symbol information for a file.""" # Get overview of the user_management.py file @@ -451,25 +444,3 @@ class TestLanguageServerSymbols: # Verify that we have entries for both files symbol_names = {LanguageServerSymbol(s_info).name for s_info in overview} assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names) - - @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_containing_symbol_of_var_is_file(self, language_server: SolidLanguageServer) -> None: - """Test that the containing symbol of a variable is the file itself.""" - # Get the containing symbol of a variable in a file - file_path = os.path.join("test_repo", "services.py") - # import of typing - references_to_typing = [ - ref.symbol - for ref in language_server.request_referencing_symbols(file_path, 4, 6, include_imports=False, include_file_symbols=True) - ] - assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File} - - # now include bodies - references_to_typing = [ - ref.symbol - for ref in language_server.request_referencing_symbols( - file_path, 4, 6, include_imports=False, include_file_symbols=True, include_body=True - ) - ] - assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File} - assert references_to_typing[0]["body"] diff --git a/test/solidlsp/r/test_r_basic.py b/test/solidlsp/r/test_r_basic.py index 647ff6c1..76f82602 100644 --- a/test/solidlsp/r/test_r_basic.py +++ b/test/solidlsp/r/test_r_basic.py @@ -3,14 +3,18 @@ Basic tests for R Language Server integration """ import os +import shutil from pathlib import Path import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("R") is None and not is_ci, reason="R is not available") @pytest.mark.r class TestRLanguageServer: """Test basic functionality of the R language server.""" @@ -82,8 +86,6 @@ class TestRLanguageServer: def test_file_matching(self): """Test that R files are properly matched.""" - from solidlsp.ls_config import Language - matcher = Language.R.get_source_fn_matcher() assert matcher.is_relevant_filename("script.R") @@ -95,3 +97,16 @@ class TestRLanguageServer: """Test R language enum value.""" assert Language.R == "r" assert str(Language.R) == "r" + + @pytest.mark.parametrize("language_server", [Language.R], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/rego/test_rego_basic.py b/test/solidlsp/rego/test_rego_basic.py index 79155a0c..ce8028c2 100644 --- a/test/solidlsp/rego/test_rego_basic.py +++ b/test/solidlsp/rego/test_rego_basic.py @@ -1,14 +1,19 @@ """Tests for Rego language server (Regal) functionality.""" import os +import shutil import pytest from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind from solidlsp.ls_utils import SymbolUtils +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("regal") is None and not is_ci, reason="Regal is not available") @pytest.mark.rego class TestRegoLanguageServer: """Test Regal language server functionality for Rego.""" @@ -132,3 +137,18 @@ class TestRegoLanguageServer: assert "validate_user_input" in symbol_names, "validate_user_input rule not found" assert "has_valid_credentials" in symbol_names, "has_valid_credentials function not found" assert "validate_request" in symbol_names, "validate_request rule not found" + + @pytest.mark.parametrize("language_server", [Language.REGO], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] == SymbolKind.Package: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/ruby/test_ruby_basic.py b/test/solidlsp/ruby/test_ruby_basic.py index b7a4cf75..2a76b4f8 100644 --- a/test/solidlsp/ruby/test_ruby_basic.py +++ b/test/solidlsp/ruby/test_ruby_basic.py @@ -6,6 +6,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.ruby @@ -37,8 +38,36 @@ class TestRubyLanguageServer: str(repo_path / "main.rb"), 16, 17 ) # add method at line 17 (0-indexed 16), position 17 - assert len(definition_location_list) == 1 - definition_location = definition_location_list[0] + unique_locations = { + ( + location["relativePath"], + location["range"]["start"]["line"], + location["range"]["start"]["character"], + location["range"]["end"]["line"], + location["range"]["end"]["character"], + ): location + for location in definition_location_list + } + + assert len(unique_locations) == 1 + definition_location = next(iter(unique_locations.values())) print(f"Found definition: {definition_location}") assert definition_location["uri"].endswith("lib.rb") assert definition_location["range"]["start"]["line"] == 1 # add method on line 2 (0-indexed 1) + + @pytest.mark.parametrize("language_server", [Language.RUBY], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name( + s, + whitespace_allowed=s["name"] == "<< self", + period_allowed=s["name"].startswith("self."), + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/rust/test_rust_basic.py b/test/solidlsp/rust/test_rust_basic.py index 8d84d736..814892d0 100644 --- a/test/solidlsp/rust/test_rust_basic.py +++ b/test/solidlsp/rust/test_rust_basic.py @@ -5,6 +5,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.rust @@ -55,3 +56,16 @@ class TestRustLanguageServer: symbols = language_server.request_full_symbol_tree() assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main missing from overview" assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add missing from overview" + + @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/solidity/test_solidity_basic.py b/test/solidlsp/solidity/test_solidity_basic.py index 348c460b..e095871f 100644 --- a/test/solidlsp/solidity/test_solidity_basic.py +++ b/test/solidlsp/solidity/test_solidity_basic.py @@ -13,6 +13,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols def _find_identifier_position(file_path: Path, symbol_name: str) -> Optional[tuple[int, int]]: @@ -163,3 +164,16 @@ class TestSolidityLanguageServerBasics: ref_files = {ref.get("uri", "") for ref in references} assert any("Token.sol" in uri for uri in ref_files), "IERC20.transfer references should include Token.sol" + + @pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/swift/test_swift_basic.py b/test/solidlsp/swift/test_swift_basic.py index 3f713f87..5a0d8bd0 100644 --- a/test/solidlsp/swift/test_swift_basic.py +++ b/test/solidlsp/swift/test_swift_basic.py @@ -15,6 +15,7 @@ from serena.util.text_utils import LineType from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols # Skip Swift tests on Windows due to complex GitHub Actions configuration WINDOWS_SKIP = platform.system() == "Windows" @@ -224,3 +225,16 @@ class TestSwiftProjectBasics: # Should find Status enum status_matches = [m for m in matches if "Status" in str(m)] assert len(status_matches) > 0, "Should find Status enum" + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/systemverilog/test_systemverilog_basic.py b/test/solidlsp/systemverilog/test_systemverilog_basic.py index b3a8a4c5..4653a84c 100644 --- a/test/solidlsp/systemverilog/test_systemverilog_basic.py +++ b/test/solidlsp/systemverilog/test_systemverilog_basic.py @@ -5,6 +5,7 @@ This module tests Language.SYSTEMVERILOG using verible-verilog-ls. Tests are skipped if the language server is not available. """ +import shutil from typing import Any import pytest @@ -12,6 +13,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols def _find_symbol_by_name(language_server: SolidLanguageServer, file_path: str, name: str) -> dict[str, Any] | None: @@ -262,3 +264,17 @@ class TestSystemVerilogRename: for uri, file_edits in changes.items(): for edit in file_edits: assert edit["newText"] == "my_counter", f"Expected 'my_counter', got {edit['newText']}" + + @pytest.mark.skipif(shutil.which("verible-verilog-ls") is None, reason="verible-verilog-ls is not available") + @pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/terraform/test_terraform_basic.py b/test/solidlsp/terraform/test_terraform_basic.py index d608e4e5..612e3f9a 100644 --- a/test/solidlsp/terraform/test_terraform_basic.py +++ b/test/solidlsp/terraform/test_terraform_basic.py @@ -5,12 +5,18 @@ These tests validate the functionality of the language server APIs like request_references using the test repository. """ +import shutil + import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from solidlsp.ls_types import SymbolKind +from test.conftest import is_ci +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +@pytest.mark.skipif(shutil.which("terraform") is None and not is_ci, reason="Terraform CLI is not available") @pytest.mark.terraform class TestLanguageServerBasics: """Test basic functionality of the Terraform language server.""" @@ -51,3 +57,18 @@ class TestLanguageServerBasics: sel_start = var_symbol["selectionRange"]["start"] references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) assert len(references) >= 1, "variable should be referenced at least once" + + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if s["kind"] == SymbolKind.Class: + continue + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/toml/test_toml_basic.py b/test/solidlsp/toml/test_toml_basic.py index b217aef8..522f8407 100644 --- a/test/solidlsp/toml/test_toml_basic.py +++ b/test/solidlsp/toml/test_toml_basic.py @@ -11,6 +11,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.toml @@ -233,3 +234,16 @@ class TestTomlLanguageServerBasics: strict_symbol = next((s for s in pyproject_symbols if s.get("name") == "strict"), None) if strict_symbol: assert strict_symbol.get("kind") == 17, "'strict' should have kind 17 (boolean)" + + @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/typescript/test_typescript_basic.py b/test/solidlsp/typescript/test_typescript_basic.py index 58c560f0..ba86ba04 100644 --- a/test/solidlsp/typescript/test_typescript_basic.py +++ b/test/solidlsp/typescript/test_typescript_basic.py @@ -5,6 +5,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.typescript @@ -31,3 +32,16 @@ class TestTypescriptLanguageServer: assert any("index.ts" in ref.get("relativePath", "") for ref in refs), ( "index.ts should reference helperFunction (tried all positions in selectionRange)" ) + + @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/vue/test_vue_basic.py b/test/solidlsp/vue/test_vue_basic.py index ca6a5e19..d3e05356 100644 --- a/test/solidlsp/vue/test_vue_basic.py +++ b/test/solidlsp/vue/test_vue_basic.py @@ -5,6 +5,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.vue @@ -360,3 +361,25 @@ class TestVueEdgeCases: f"Composable '{expected_composable}' should appear exactly once in directory overview. " f"Found {len(matching_files)} matches. All files: {list(composables_overview.keys())}" ) + + @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + is_callback = s["name"].endswith(" callback") + is_vue_selector = "." in s["name"] or ":" in s["name"] + is_vue_block = s["name"] in {"script setup", "style scoped"} + if has_malformed_name( + s, + whitespace_allowed=is_callback or is_vue_selector or is_vue_block, + period_allowed=is_vue_selector, + colon_allowed=is_vue_selector, + parenthesis_allowed=is_callback or is_vue_selector, + ): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/yaml_ls/test_yaml_basic.py b/test/solidlsp/yaml_ls/test_yaml_basic.py index ded7f01a..da60e487 100644 --- a/test/solidlsp/yaml_ls/test_yaml_basic.py +++ b/test/solidlsp/yaml_ls/test_yaml_basic.py @@ -11,6 +11,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.yaml @@ -175,3 +176,16 @@ class TestYAMLLanguageServerBasics: app_port = next((s for s in port_symbols if s["range"]["start"]["line"] == 4), None) assert app_port is not None, "Should find 'port' under 'app'" assert app_port["range"]["start"]["character"] == 2, "'port' should be indented 2 spaces" + + @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/zig/test_zig_basic.py b/test/solidlsp/zig/test_zig_basic.py index 12e23f8c..51b23650 100644 --- a/test/solidlsp/zig/test_zig_basic.py +++ b/test/solidlsp/zig/test_zig_basic.py @@ -13,6 +13,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @pytest.mark.zig @@ -341,3 +342,16 @@ class TestZigLanguageServer: root = symbols[0] assert isinstance(root, dict), "Root should be a dict" assert "name" in root, "Root should have a name" + + @pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True) + def test_bare_symbol_names(self, language_server) -> None: + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, whitespace_allowed=True): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + )