From 71d23277dae52a5afbff0ae974e4814b42ab13b4 Mon Sep 17 00:00:00 2001 From: Roman Nikulenkov Date: Mon, 5 Jan 2026 15:58:27 +0300 Subject: [PATCH 1/3] feat(gopls): support gopls_settings via ls_specific_settings - Forward ls_specific_settings.go.gopls_settings to gopls as LSP initializationOptions (JSON-validated) - Add cache context fingerprinting so persisted symbol caches are invalidated when Go build context changes (e.g. buildFlags, GOFLAGS, GOOS, GOARCH, CGO_ENABLED) - Add build-tag fixtures + tests covering tag-dependent symbols and cache invalidation - Document ls_specific_settings + Go example in configuration docs --- docs/02-usage/050_configuration.md | 41 +++++ src/solidlsp/language_servers/gopls.py | 83 ++++++++- src/solidlsp/ls.py | 35 +++- test/conftest.py | 24 ++- .../repos/go/test_repo/buildtags/foo.go | 8 + .../repos/go/test_repo/buildtags/notfoo.go | 8 + test/resources/repos/go/test_repo/go.mod | 3 + test/solidlsp/go/test_go_basic.py | 157 +++++++++++++++++- 8 files changed, 344 insertions(+), 15 deletions(-) create mode 100644 test/resources/repos/go/test_repo/buildtags/foo.go create mode 100644 test/resources/repos/go/test_repo/buildtags/notfoo.go create mode 100644 test/resources/repos/go/test_repo/go.mod diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index 1a62f6b0..68a99284 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -134,6 +134,47 @@ For advanced users, Serena's configuration can be further customized. The Serena user data directory (where configuration, language server files, logs, etc. are stored) defaults to `~/.serena`. You can change this location by setting the `SERENA_HOME` environment variable to your desired path. +### Language server specific settings (`ls_specific_settings`) + +`ls_specific_settings` lets you pass per-language, language-server-specific configuration. + +Structure: + +```yaml +ls_specific_settings: + : + # language-server-specific keys +``` + +Where to put it: +- Global default: `~/.serena/serena_config.yml` +- Per-project override: `/.serena/project.yml` (takes precedence over the global config for the same keys) +- There is currently no `serena-mcp-server` CLI option to set `ls_specific_settings`; use the config files above. + +#### Go (`gopls`) + +Serena forwards `ls_specific_settings.go.gopls_settings` to `gopls` as LSP `initializationOptions` when the Go language server is started. + +Example: enable build tags and set a build environment: + +```yaml +ls_specific_settings: + go: + gopls_settings: + buildFlags: + - "-tags=foo" + env: + GOOS: "linux" + GOARCH: "amd64" + CGO_ENABLED: "0" +``` + +Notes: +- To enable multiple tags, use `"-tags=foo,bar"`. +- `gopls_settings.env` values are strings. +- `GOFLAGS` (from the environment you start Serena in) may also affect the Go build context. Prefer `buildFlags` for tags. +- Build context changes are only picked up when `gopls` starts. After changing `gopls_settings` (or relevant env vars like `GOFLAGS`), restart the Serena process (or server) that hosts the Go language server, or use your client's "Restart language server" action if it causes `gopls` to restart. + ### Custom Prompts All of Serena's prompts can be fully customized. diff --git a/src/solidlsp/language_servers/gopls.py b/src/solidlsp/language_servers/gopls.py index afb628b9..aaa59793 100644 --- a/src/solidlsp/language_servers/gopls.py +++ b/src/solidlsp/language_servers/gopls.py @@ -98,13 +98,12 @@ class Gopls(SolidLanguageServer): self.server_ready = threading.Event() self.request_id = 0 - @staticmethod - def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: """ Returns the initialize params for the Go Language Server. """ root_uri = pathlib.Path(repository_absolute_path).as_uri() - initialize_params = { + initialize_params: dict = { "locale": "en", "capabilities": { "textDocument": { @@ -128,8 +127,86 @@ class Gopls(SolidLanguageServer): } ], } + + # Apply gopls-specific settings via initializationOptions + # Serena applies gopls settings at initialization time via initializationOptions + # (Access settings directly to avoid extra INFO logging from CustomLSSettings.get.) + gopls_settings = self._custom_settings.settings.get("gopls_settings") + if gopls_settings: + if not isinstance(gopls_settings, dict): + raise TypeError( + f"gopls_settings must be a dict, got {type(gopls_settings).__name__}. " + "Expected structure: {'buildFlags': ['-tags=foo'], 'env': {...}, ...}" + ) + + # Validate JSON-serializability early: initializationOptions is sent over JSON-RPC. + import json + + try: + json.dumps(gopls_settings, sort_keys=True, separators=(",", ":")) + except (TypeError, ValueError) as exc: + raise TypeError( + "gopls_settings must be JSON-serializable (json.dumps). Use JSON-compatible values (dict/list/str/int/float/bool/null) and prefer string keys." + ) from exc + + # Log keys only (and at DEBUG) to avoid leaking sensitive values and to reduce startup noise. + log.debug("Applying gopls settings via initializationOptions: keys=%s", list(gopls_settings.keys())) + initialize_params["initializationOptions"] = gopls_settings + return cast(InitializeParams, initialize_params) + # Environment variables that influence Go build context and affect cached symbols. + _CACHE_CONTEXT_ENV_KEYS = ("GOFLAGS", "GOOS", "GOARCH", "CGO_ENABLED") + + @override + def _cache_context_fingerprint(self) -> str | None: + """ + 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 + + gopls_settings_raw = self._custom_settings.settings.get("gopls_settings") + + gopls_settings: dict | None + if gopls_settings_raw is None: + gopls_settings = None + else: + if not isinstance(gopls_settings_raw, dict): + raise TypeError( + f"gopls_settings must be a dict, got {type(gopls_settings_raw).__name__}. " + "Expected structure: {'buildFlags': ['-tags=foo'], 'env': {...}, ...}" + ) + + # Treat an explicitly empty dict the same as not providing settings at all. + gopls_settings = gopls_settings_raw or None + + # Only include env vars that are set to a non-empty value. + env_subset: dict[str, str] = {} + for key in self._CACHE_CONTEXT_ENV_KEYS: + value = os.environ.get(key) + if value: + env_subset[key] = value + + # Return None only when BOTH settings and env subset are effectively empty. + if gopls_settings is None and not env_subset: + return None + + fingerprint_data: dict[str, object] = {"env": env_subset} + if gopls_settings is not None: + fingerprint_data["gopls_settings"] = gopls_settings + + try: + canonical_json = json.dumps(fingerprint_data, sort_keys=True, separators=(",", ":")) + except (TypeError, ValueError) as exc: + raise TypeError( + "gopls_settings must be JSON-serializable (json.dumps). Use JSON-compatible values (dict/list/str/int/float/bool/null) and prefer string keys." + ) from exc + + return hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()[:16] + def _start_server(self) -> None: """Start gopls server process""" diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index be66e36c..23058e2b 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1699,6 +1699,29 @@ class SolidLanguageServer(ABC): return defining_symbol + def _cache_context_fingerprint(self) -> Hashable | None: + """ + Return a fingerprint of any language-server-specific context that affects cached results. + + Subclasses may override to provide a deterministic value that changes when cached results + would be invalidated (e.g., build flags, environment variables). + + The value must be hashable and safe for inclusion in cache version tuples. + Returns None if no context-specific fingerprint is needed. + """ + return None + + def _document_symbols_cache_version(self) -> Hashable: + """ + Return the version for the document symbols cache. + + Incorporates cache context fingerprint if provided by the language server. + """ + fingerprint = self._cache_context_fingerprint() + if fingerprint is not None: + return (self.DOCUMENT_SYMBOL_CACHE_VERSION, fingerprint) + return self.DOCUMENT_SYMBOL_CACHE_VERSION + def _save_raw_document_symbols_cache(self) -> None: cache_file = self.cache_dir / self.RAW_DOCUMENT_SYMBOL_CACHE_FILENAME @@ -1717,8 +1740,12 @@ class SolidLanguageServer(ABC): e, ) - def _raw_document_symbols_cache_version(self) -> tuple[int, Hashable]: - return (self.RAW_DOCUMENT_SYMBOLS_CACHE_VERSION, self._ls_specific_raw_document_symbols_cache_version) + def _raw_document_symbols_cache_version(self) -> tuple[Hashable, ...]: + base_version: tuple[Hashable, ...] = (self.RAW_DOCUMENT_SYMBOLS_CACHE_VERSION, self._ls_specific_raw_document_symbols_cache_version) + fingerprint = self._cache_context_fingerprint() + if fingerprint is not None: + return (*base_version, fingerprint) + return base_version def _load_raw_document_symbols_cache(self) -> None: cache_file = self.cache_dir / self.RAW_DOCUMENT_SYMBOL_CACHE_FILENAME @@ -1774,7 +1801,7 @@ class SolidLanguageServer(ABC): log.info("Saving updated document symbols cache to %s", cache_file) try: - save_cache(str(cache_file), self.DOCUMENT_SYMBOL_CACHE_VERSION, self._document_symbols_cache) + save_cache(str(cache_file), self._document_symbols_cache_version(), self._document_symbols_cache) self._document_symbols_cache_is_modified = False except Exception as e: log.error( @@ -1788,7 +1815,7 @@ class SolidLanguageServer(ABC): if cache_file.exists(): log.info("Loading document symbols cache from %s", cache_file) try: - saved_cache = load_cache(str(cache_file), self.DOCUMENT_SYMBOL_CACHE_VERSION) + saved_cache = load_cache(str(cache_file), self._document_symbols_cache_version()) if saved_cache is not None: self._document_symbols_cache = saved_cache log.info(f"Loaded {len(self._document_symbols_cache)} entries from document symbols cache.") diff --git a/test/conftest.py b/test/conftest.py index 84aa80f3..e02b428c 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,10 +1,11 @@ import logging import os from collections.abc import Iterator +from contextlib import contextmanager from pathlib import Path +from typing import Any import pytest -from blib2to3.pgen2.parse import contextmanager from sensai.util.logging import configure from serena.config.serena_config import SerenaPaths @@ -38,7 +39,12 @@ def get_repo_path(language: Language) -> Path: def _create_ls( - language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None, trace_lsp_communication: bool = False + language: Language, + repo_path: str | None = None, + ignored_paths: list[str] | None = None, + trace_lsp_communication: bool = False, + ls_specific_settings: dict[Language, dict[str, Any]] | None = None, + solidlsp_dir: Path | None = None, ) -> SolidLanguageServer: ignored_paths = ignored_paths or [] if repo_path is None: @@ -47,20 +53,28 @@ def _create_ls( for spec in gitignore_parser.get_ignore_specs(): ignored_paths.extend(spec.patterns) config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) + effective_solidlsp_dir = solidlsp_dir if solidlsp_dir is not None else SerenaPaths().serena_user_home_dir return SolidLanguageServer.create( config, repo_path, solidlsp_settings=SolidLSPSettings( - solidlsp_dir=SerenaPaths().serena_user_home_dir, project_data_relative_path=SERENA_MANAGED_DIR_NAME + solidlsp_dir=effective_solidlsp_dir, + project_data_relative_path=SERENA_MANAGED_DIR_NAME, + ls_specific_settings=ls_specific_settings or {}, ), ) @contextmanager def start_ls_context( - language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None, trace_lsp_communication: bool = False + language: Language, + repo_path: str | None = None, + ignored_paths: list[str] | None = None, + trace_lsp_communication: bool = False, + ls_specific_settings: dict[Language, dict[str, Any]] | None = None, + solidlsp_dir: Path | None = None, ) -> Iterator[SolidLanguageServer]: - ls = _create_ls(language, repo_path, ignored_paths, trace_lsp_communication) + ls = _create_ls(language, repo_path, ignored_paths, trace_lsp_communication, ls_specific_settings, solidlsp_dir) log.info(f"Starting language server for {language} {repo_path}") ls.start() try: diff --git a/test/resources/repos/go/test_repo/buildtags/foo.go b/test/resources/repos/go/test_repo/buildtags/foo.go new file mode 100644 index 00000000..43df3dc6 --- /dev/null +++ b/test/resources/repos/go/test_repo/buildtags/foo.go @@ -0,0 +1,8 @@ +//go:build foo +// +build foo + +package buildtags + +type XFoo struct { + Value int +} diff --git a/test/resources/repos/go/test_repo/buildtags/notfoo.go b/test/resources/repos/go/test_repo/buildtags/notfoo.go new file mode 100644 index 00000000..831a63b5 --- /dev/null +++ b/test/resources/repos/go/test_repo/buildtags/notfoo.go @@ -0,0 +1,8 @@ +//go:build !foo +// +build !foo + +package buildtags + +type XNotFoo struct { + Value int +} diff --git a/test/resources/repos/go/test_repo/go.mod b/test/resources/repos/go/test_repo/go.mod new file mode 100644 index 00000000..59623c28 --- /dev/null +++ b/test/resources/repos/go/test_repo/go.mod @@ -0,0 +1,3 @@ +module test_repo + +go 1.21 diff --git a/test/solidlsp/go/test_go_basic.py b/test/solidlsp/go/test_go_basic.py index eac511e9..f1255eba 100644 --- a/test/solidlsp/go/test_go_basic.py +++ b/test/solidlsp/go/test_go_basic.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import pytest @@ -28,6 +29,156 @@ class TestGoLanguageServer: assert helper_symbol is not None, "Could not find 'Helper' function symbol in main.go" sel_start = helper_symbol["selectionRange"]["start"] refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) - assert any( - "main.go" in ref.get("relativePath", "") for ref in refs - ), "main.go should reference Helper (tried all positions in selectionRange)" + assert any("main.go" in ref.get("uri", "") for ref in refs), "Expected at least one reference result to point at main.go" + + +def _filter_symbols_by_name_in_repo(symbols: list | None, target_name: str, repo_name: str = "test_repo") -> list: + """Filter workspace symbols to exact name matches in the test repo.""" + if symbols is None: + return [] + return [s for s in symbols if s.get("name") == target_name and repo_name in s.get("location", {}).get("uri", "")] + + +@pytest.mark.go +class TestGoBuildTags: + """Tests for Go build tag/constraint support.""" + + def _copy_go_fixture(self, tmp_path: Path) -> Path: + """Copy Go fixture repo into tmp_path.""" + import shutil + + from test.conftest import get_repo_path + + fixture_path = get_repo_path(Language.GO) + target_path = tmp_path / "test_repo" + + shutil.copytree(fixture_path, target_path) + return target_path + + def test_default_context_contains_xnotfoo(self, tmp_path: Path) -> None: + """Default build context should contain XNotFoo and not XFoo.""" + from test.conftest import start_ls_context + + repo_path = self._copy_go_fixture(tmp_path) + + with start_ls_context(Language.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls: + xnotfoo_symbols = ls.request_workspace_symbol("XNotFoo") + xfoo_symbols = ls.request_workspace_symbol("XFoo") + + xnotfoo_matches = _filter_symbols_by_name_in_repo(xnotfoo_symbols, "XNotFoo") + xfoo_matches = _filter_symbols_by_name_in_repo(xfoo_symbols, "XFoo") + + assert len(xnotfoo_matches) > 0, "Default context should contain XNotFoo" + assert len(xfoo_matches) == 0, "Default context should NOT contain XFoo" + + def test_foo_context_contains_xfoo(self, tmp_path: Path) -> None: + """Build context with -tags=foo should contain XFoo and not XNotFoo.""" + from test.conftest import start_ls_context + + repo_path = self._copy_go_fixture(tmp_path) + + ls_settings = { + Language.GO: { + "gopls_settings": { + "buildFlags": ["-tags=foo"], + }, + }, + } + + with start_ls_context(Language.GO, repo_path=str(repo_path), ls_specific_settings=ls_settings, solidlsp_dir=tmp_path) as ls: + xfoo_symbols = ls.request_workspace_symbol("XFoo") + xnotfoo_symbols = ls.request_workspace_symbol("XNotFoo") + + xfoo_matches = _filter_symbols_by_name_in_repo(xfoo_symbols, "XFoo") + xnotfoo_matches = _filter_symbols_by_name_in_repo(xnotfoo_symbols, "XNotFoo") + + assert len(xfoo_matches) > 0, "Foo context should contain XFoo" + assert len(xnotfoo_matches) == 0, "Foo context should NOT contain XNotFoo" + + def test_disk_cache_is_invalidated_on_build_context_switch(self, tmp_path: Path) -> None: + """Go build context switches must not reuse persisted SolidLSP document-symbol caches.""" + import pickle + + from test.conftest import start_ls_context + + repo_path = self._copy_go_fixture(tmp_path) + + ls_settings_foo = { + Language.GO: { + "gopls_settings": { + "buildFlags": ["-tags=foo"], + }, + }, + } + + main_go = os.path.join("main.go") + + # Run 1 (default context): populate caches and persist them to disk. + with start_ls_context(Language.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default: + _ = ls_default.request_document_symbols(main_go) + + default_raw_cache_version = ls_default._raw_document_symbols_cache_version() + default_doc_cache_version = ls_default._document_symbols_cache_version() + + ls_default.save_cache() + cache_dir = ls_default.cache_dir + + cache_files = [p for p in cache_dir.rglob("*") if p.is_file()] + assert cache_files, f"Expected SolidLSP to create cache artifacts under {cache_dir}" + + versioned_cache_files: list[tuple[Path, object]] = [] + for p in cache_files: + try: + with p.open("rb") as f: + data = pickle.load(f) + except Exception: + continue + if isinstance(data, dict) and "__cache_version" in data: + versioned_cache_files.append((p, data["__cache_version"])) + + assert versioned_cache_files, f"Expected at least one SolidLSP cache file with a __cache_version under {cache_dir}" + saved_versions = {v for _, v in versioned_cache_files} + assert ( + default_raw_cache_version in saved_versions or default_doc_cache_version in saved_versions + ), "Expected at least one persisted cache to match the default-context cache version" + + # Run 2 (default context again): prove that persisted caches are actually loaded and used. + with start_ls_context(Language.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again: + assert ls_default_again.cache_dir == cache_dir + + assert ls_default_again._raw_document_symbols_cache, "Expected raw document-symbol cache to load from disk" + assert ls_default_again._document_symbols_cache, "Expected document-symbol cache to load from disk" + + assert not ls_default_again._raw_document_symbols_cache_is_modified + assert not ls_default_again._document_symbols_cache_is_modified + + _ = ls_default_again.request_document_symbols(main_go) + + # A cache hit should not mark caches as modified. + assert not ls_default_again._raw_document_symbols_cache_is_modified + assert not ls_default_again._document_symbols_cache_is_modified + + # Run 3 (foo context): the same on-disk cache directory exists, but MUST be treated as stale. + with start_ls_context( + Language.GO, + repo_path=str(repo_path), + ls_specific_settings=ls_settings_foo, + solidlsp_dir=tmp_path, + ) as ls_foo: + assert ls_foo.cache_dir == cache_dir + + foo_raw_cache_version = ls_foo._raw_document_symbols_cache_version() + foo_doc_cache_version = ls_foo._document_symbols_cache_version() + + assert foo_raw_cache_version != default_raw_cache_version + assert foo_doc_cache_version != default_doc_cache_version + + # Different build context => persisted caches must not be loaded. + assert ls_foo._raw_document_symbols_cache == {} + assert ls_foo._document_symbols_cache == {} + + _ = ls_foo.request_document_symbols(main_go) + + # A cache miss should repopulate and mark caches modified. + assert ls_foo._raw_document_symbols_cache_is_modified + assert ls_foo._document_symbols_cache_is_modified From 1643c14aaecb77a001a11a3199c8f2c5759019df Mon Sep 17 00:00:00 2001 From: Roman Nikulenkov Date: Mon, 5 Jan 2026 17:00:54 +0300 Subject: [PATCH 2/3] refactor(gopls): streamline gopls settings validation and JSON serialization test(go): add assertions for SolidLanguageServer cache states --- src/solidlsp/language_servers/gopls.py | 47 ++++++++++++-------------- test/solidlsp/go/test_go_basic.py | 28 ++++++++++----- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/solidlsp/language_servers/gopls.py b/src/solidlsp/language_servers/gopls.py index aaa59793..419109ef 100644 --- a/src/solidlsp/language_servers/gopls.py +++ b/src/solidlsp/language_servers/gopls.py @@ -3,7 +3,7 @@ import os import pathlib import subprocess import threading -from typing import cast +from typing import Any, cast from overrides import override @@ -133,21 +133,12 @@ class Gopls(SolidLanguageServer): # (Access settings directly to avoid extra INFO logging from CustomLSSettings.get.) gopls_settings = self._custom_settings.settings.get("gopls_settings") if gopls_settings: - if not isinstance(gopls_settings, dict): - raise TypeError( - f"gopls_settings must be a dict, got {type(gopls_settings).__name__}. " - "Expected structure: {'buildFlags': ['-tags=foo'], 'env': {...}, ...}" - ) + gopls_settings = self._validate_gopls_settings_dict(gopls_settings) # Validate JSON-serializability early: initializationOptions is sent over JSON-RPC. import json - try: - json.dumps(gopls_settings, sort_keys=True, separators=(",", ":")) - except (TypeError, ValueError) as exc: - raise TypeError( - "gopls_settings must be JSON-serializable (json.dumps). Use JSON-compatible values (dict/list/str/int/float/bool/null) and prefer string keys." - ) from exc + self._canonical_json_or_raise(json, gopls_settings) # Log keys only (and at DEBUG) to avoid leaking sensitive values and to reduce startup noise. log.debug("Applying gopls settings via initializationOptions: keys=%s", list(gopls_settings.keys())) @@ -155,6 +146,23 @@ class Gopls(SolidLanguageServer): return cast(InitializeParams, initialize_params) + def _validate_gopls_settings_dict(self, gopls_settings: object) -> dict: + if not isinstance(gopls_settings, dict): + raise TypeError( + f"gopls_settings must be a dict, got {type(gopls_settings).__name__}. " + "Expected structure: {'buildFlags': ['-tags=foo'], 'env': {...}, ...}" + ) + + return gopls_settings + + def _canonical_json_or_raise(self, json_module: Any, data: object) -> str: + try: + return json_module.dumps(data, sort_keys=True, separators=(",", ":")) + except (TypeError, ValueError) as exc: + raise TypeError( + "gopls_settings must be JSON-serializable (json.dumps). Use JSON-compatible values (dict/list/str/int/float/bool/null) and prefer string keys." + ) from exc + # Environment variables that influence Go build context and affect cached symbols. _CACHE_CONTEXT_ENV_KEYS = ("GOFLAGS", "GOOS", "GOARCH", "CGO_ENABLED") @@ -174,14 +182,8 @@ class Gopls(SolidLanguageServer): if gopls_settings_raw is None: gopls_settings = None else: - if not isinstance(gopls_settings_raw, dict): - raise TypeError( - f"gopls_settings must be a dict, got {type(gopls_settings_raw).__name__}. " - "Expected structure: {'buildFlags': ['-tags=foo'], 'env': {...}, ...}" - ) - # Treat an explicitly empty dict the same as not providing settings at all. - gopls_settings = gopls_settings_raw or None + gopls_settings = self._validate_gopls_settings_dict(gopls_settings_raw) or None # Only include env vars that are set to a non-empty value. env_subset: dict[str, str] = {} @@ -198,12 +200,7 @@ class Gopls(SolidLanguageServer): if gopls_settings is not None: fingerprint_data["gopls_settings"] = gopls_settings - try: - canonical_json = json.dumps(fingerprint_data, sort_keys=True, separators=(",", ":")) - except (TypeError, ValueError) as exc: - raise TypeError( - "gopls_settings must be JSON-serializable (json.dumps). Use JSON-compatible values (dict/list/str/int/float/bool/null) and prefer string keys." - ) from exc + canonical_json = self._canonical_json_or_raise(json, fingerprint_data) return hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()[:16] diff --git a/test/solidlsp/go/test_go_basic.py b/test/solidlsp/go/test_go_basic.py index f1255eba..cd7e8a86 100644 --- a/test/solidlsp/go/test_go_basic.py +++ b/test/solidlsp/go/test_go_basic.py @@ -113,6 +113,22 @@ class TestGoBuildTags: main_go = os.path.join("main.go") + def _assert_caches_loaded_and_clean(ls: SolidLanguageServer) -> None: + # White-box assertions: SolidLanguageServer currently has no public API to verify that + # caches were loaded from disk vs created lazily on first request. + assert ls._raw_document_symbols_cache, "Expected raw document-symbol cache to load from disk" + assert ls._document_symbols_cache, "Expected document-symbol cache to load from disk" + assert not ls._raw_document_symbols_cache_is_modified + assert not ls._document_symbols_cache_is_modified + + def _assert_caches_empty(ls: SolidLanguageServer) -> None: + assert ls._raw_document_symbols_cache == {} + assert ls._document_symbols_cache == {} + + def _assert_caches_modified(ls: SolidLanguageServer) -> None: + assert ls._raw_document_symbols_cache_is_modified + assert ls._document_symbols_cache_is_modified + # Run 1 (default context): populate caches and persist them to disk. with start_ls_context(Language.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default: _ = ls_default.request_document_symbols(main_go) @@ -146,11 +162,7 @@ class TestGoBuildTags: with start_ls_context(Language.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again: assert ls_default_again.cache_dir == cache_dir - assert ls_default_again._raw_document_symbols_cache, "Expected raw document-symbol cache to load from disk" - assert ls_default_again._document_symbols_cache, "Expected document-symbol cache to load from disk" - - assert not ls_default_again._raw_document_symbols_cache_is_modified - assert not ls_default_again._document_symbols_cache_is_modified + _assert_caches_loaded_and_clean(ls_default_again) _ = ls_default_again.request_document_symbols(main_go) @@ -174,11 +186,9 @@ class TestGoBuildTags: assert foo_doc_cache_version != default_doc_cache_version # Different build context => persisted caches must not be loaded. - assert ls_foo._raw_document_symbols_cache == {} - assert ls_foo._document_symbols_cache == {} + _assert_caches_empty(ls_foo) _ = ls_foo.request_document_symbols(main_go) # A cache miss should repopulate and mark caches modified. - assert ls_foo._raw_document_symbols_cache_is_modified - assert ls_foo._document_symbols_cache_is_modified + _assert_caches_modified(ls_foo) From 51829c5866c153a2b14655c9d72a6eccee52bf4d Mon Sep 17 00:00:00 2001 From: Roman Nikulenkov Date: Mon, 5 Jan 2026 17:47:34 +0300 Subject: [PATCH 3/3] docs: add copilot instructions referencing CLAUDE.md --- .github/copilot-instructions.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..d359e4f7 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +MUST use the project-specific instructions from the `CLAUDE.md` file located in the project root. \ No newline at end of file