From 8691ae30462dbe9fc928794194c9f04e180543f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:21:13 +0000 Subject: [PATCH 1/5] Bump python-multipart in the uv group across 1 directory Bumps the uv group with 1 update in the / directory: [python-multipart](https://github.com/Kludex/python-multipart). Updates `python-multipart` from 0.0.22 to 0.0.26 - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kludex/python-multipart/compare/0.0.22...0.0.26) --- updated-dependencies: - dependency-name: python-multipart dependency-version: 0.0.26 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b073ca20..e09bf469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "urllib3==2.6.3", "werkzeug==3.1.7", "starlette==1.0.0", - "python-multipart==0.0.22", + "python-multipart==0.0.26", "filelock==3.25.2", "cryptography==46.0.7", "regex==2026.2.28", diff --git a/uv.lock b/uv.lock index 7802365c..793e80cb 100644 --- a/uv.lock +++ b/uv.lock @@ -2421,11 +2421,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] @@ -3060,7 +3060,7 @@ requires-dist = [ { name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = "==3.8.0" }, { name = "python-dotenv", specifier = "==1.2.1" }, - { name = "python-multipart", specifier = "==0.0.22" }, + { name = "python-multipart", specifier = "==0.0.26" }, { name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0rc0" }, { name = "pywebview", specifier = "==6.2" }, { name = "pyyaml", specifier = "==6.0.2" }, From 9d64931fd98fc47d387abe2b2214c71eb7728cf6 Mon Sep 17 00:00:00 2001 From: ek-133 Date: Thu, 16 Apr 2026 12:11:53 +0300 Subject: [PATCH 2/5] Issue #1348 Vue rename_symbol fix (#1355) Edits in vue files need to be propagated to the TS LS, e.g. for cross-language renames --- CHANGELOG.md | 3 + .../language_servers/vue_language_server.py | 67 ++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e56290..9fb21277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Status of the `main` branch. Changes prior to the next official version change w * Dependencies: - `pywebview`: Switch back to official release (new version 6.2) #1253 +* Language Servers: + - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files + # v1.1.2 (2026-04-14) * General: diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py index 95d08732..06123623 100644 --- a/src/solidlsp/language_servers/vue_language_server.py +++ b/src/solidlsp/language_servers/vue_language_server.py @@ -8,7 +8,8 @@ import os import pathlib import shutil import threading -from pathlib import Path +from collections.abc import Callable +from pathlib import Path, PurePath from time import sleep from typing import Any @@ -425,6 +426,70 @@ class VueLanguageServer(SolidLanguageServer): with self._ts_server.open_file(relative_file_path): return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name) + def _forward_edit_to_ts_server_if_needed(self, relative_file_path: str, edit_fn: Callable[[], object]) -> None: + """ + Calls ``edit_fn`` on the TypeScript server if the file is open there. + + Only applicable to non-TypeScript files (i.e. .vue files) that have been + indexed on the TypeScript server for cross-file reference support. + + :param relative_file_path: the relative path of the file that was edited + :param edit_fn: callable that performs the corresponding edit on ``_ts_server`` + """ + if self._ts_server is None or not self._ts_server_started: + return + if self._is_typescript_file(relative_file_path): + return + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + if uri in self._ts_server.open_file_buffers: + edit_fn() + + @override + def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position: + """ + Inserts text at the given position, forwarding the change to the TypeScript server if it has the file open. + + :param relative_file_path: the relative path of the file to edit + :param line: the line number + :param column: the column number + :param text_to_be_inserted: the text to insert + :return: updated cursor position + """ + result = super().insert_text_at_position(relative_file_path, line, column, text_to_be_inserted) + self._forward_edit_to_ts_server_if_needed( + relative_file_path, + lambda: self._ts_server.insert_text_at_position( # type: ignore[union-attr] + relative_file_path, line, column, text_to_be_inserted + ), + ) + return result + + @override + def delete_text_between_positions( + self, + relative_file_path: str, + start: ls_types.Position, + end: ls_types.Position, + ) -> str: + """ + Deletes text between the given positions, forwarding the change to the TypeScript server if it has the file open. + + :param relative_file_path: the relative path of the file to edit + :param start: start position + :param end: end position + :return: deleted text + """ + deleted_text = super().delete_text_between_positions(relative_file_path, start, end) + self._forward_edit_to_ts_server_if_needed( + relative_file_path, + lambda: self._ts_server.delete_text_between_positions( # type: ignore[union-attr] + relative_file_path, start, end + ), + ) + return deleted_text + @classmethod def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[list[str], str, str]: is_node_installed = shutil.which("node") is not None From d650f30c7ed93fba475cfbdff424eb4ba639b1e0 Mon Sep 17 00:00:00 2001 From: mesa3 <43036352+mesa3@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:19:29 +0800 Subject: [PATCH 3/5] Fix clangd cache invalidation and capability checks (#1359) Summary relax clangd initialize capability checks to tolerate valid response shape differences add a clangd-specific document-symbol cache fingerprint so stale C++ symbol caches are invalidated when backend/config state changes add a regression test covering C++ document-symbol cache invalidation Problem For C++ projects, Serena's symbol tools could keep returning malformed cached document symbols even after clangd / compile commands / backend state had been repaired. The cache key was effectively stable as long as file contents did not change, so previously bad symbol trees could survive later LS recovery. In addition, clangd capability checks were too strict and assumed a single exact initialize-response shape. What this changes accepts both integer and object-style textDocumentSync responses validates completionProvider more conservatively by checking required trigger characters instead of exact equality fingerprints clangd document-symbol caches using: clangd_version ls_path compile_commands_dir compile_commands.json content hash adds a C++ regression test similar to the existing Go cache invalidation coverage --- CHANGELOG.md | 5 +- .../clangd_language_server.py | 45 ++++++++++-- test/solidlsp/cpp/test_cpp_basic.py | 71 +++++++++++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb21277..2cf145fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,12 @@ Status of the `main` branch. Changes prior to the next official version change w * Dependencies: - `pywebview`: Switch back to official release (new version 6.2) #1253 -* Language Servers: + +* Language Servers: + - Fix: clangd capability checks now tolerate valid initialize response shape differences and invalidate cached C++ document symbols when clangd/compile commands context changes #1359 - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files + # v1.1.2 (2026-04-14) * General: diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index 0210e7d5..0dc5ed9c 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -1,8 +1,10 @@ +import hashlib import json import logging import os import pathlib import threading +from collections.abc import Hashable from typing import Any, cast from overrides import override @@ -42,6 +44,30 @@ class ClangdLanguageServer(SolidLanguageServer): self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + cache_format_version = 1 + cpp_settings: dict[str, Any] = self._custom_settings or {} + return ( + cache_format_version, + cpp_settings.get("clangd_version"), + cpp_settings.get("ls_path"), + cpp_settings.get("compile_commands_dir"), + self._compile_commands_fingerprint(), + ) + + def _compile_commands_fingerprint(self) -> str | None: + compile_db_path = os.path.join(self.repository_root_path, "compile_commands.json") + if not os.path.exists(compile_db_path): + return None + + try: + with open(compile_db_path, "rb") as f: + return hashlib.md5(f.read()).hexdigest() + except OSError as e: + log.warning(f"Failed to fingerprint compile_commands.json: {e}") + return None + @override def is_ignored_dirname(self, dirname: str) -> bool: ignored_dirs = [ @@ -321,12 +347,19 @@ class ClangdLanguageServer(SolidLanguageServer): log.info("Sending initialize request from LSP client to LSP server and awaiting response") init_response = self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": [".", "<", ">", ":", '"', "/", "*"], - "resolveProvider": False, - } + capabilities = init_response["capabilities"] + + text_document_sync = capabilities["textDocumentSync"] + if isinstance(text_document_sync, int): + assert text_document_sync == 2 # type: ignore + else: + assert text_document_sync["change"] == 2 # type: ignore + + assert "completionProvider" in capabilities + completion_provider = capabilities["completionProvider"] + trigger_characters = set(completion_provider["triggerCharacters"]) + assert {".", "<", ">", ":", '"', "/"}.issubset(trigger_characters) + assert completion_provider["resolveProvider"] is False self.server.notify.initialized({}) # set ready flag, clangd sends no meaningful notification when ready diff --git a/test/solidlsp/cpp/test_cpp_basic.py b/test/solidlsp/cpp/test_cpp_basic.py index 3ca52693..dfb18881 100644 --- a/test/solidlsp/cpp/test_cpp_basic.py +++ b/test/solidlsp/cpp/test_cpp_basic.py @@ -9,12 +9,14 @@ server is not available. import os import pathlib import shutil +from pathlib import Path import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils +from test.conftest import get_repo_path, start_ls_context from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @@ -156,3 +158,72 @@ int use_add() { f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + +@pytest.mark.cpp +class TestCppDocumentSymbolCache: + def _copy_cpp_fixture(self, tmp_path: Path) -> Path: + fixture_path = get_repo_path(Language.CPP) + target_path = tmp_path / "test_repo" + shutil.copytree(fixture_path, target_path) + return target_path + + def test_cache_invalidates_when_clangd_context_changes(self, tmp_path: Path) -> None: + repo_path = self._copy_cpp_fixture(tmp_path) + ls_settings_alt = { + Language.CPP: { + "compile_commands_dir": ".serena-alt", + } + } + + main_cpp = os.path.join("a.cpp") + + def _assert_caches_loaded_and_clean(ls: SolidLanguageServer) -> None: + 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 + + with start_ls_context(Language.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default: + _ = ls_default.request_document_symbols(main_cpp) + + 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}" + + with start_ls_context(Language.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again: + assert ls_default_again.cache_dir == cache_dir + _assert_caches_loaded_and_clean(ls_default_again) + _ = ls_default_again.request_document_symbols(main_cpp) + assert not ls_default_again._raw_document_symbols_cache_is_modified + assert not ls_default_again._document_symbols_cache_is_modified + + with start_ls_context( + Language.CPP, + repo_path=str(repo_path), + ls_specific_settings=ls_settings_alt, + solidlsp_dir=tmp_path, + ) as ls_alt: + assert ls_alt.cache_dir == cache_dir + alt_raw_cache_version = ls_alt._raw_document_symbols_cache_version() + alt_doc_cache_version = ls_alt._document_symbols_cache_version() + + assert alt_raw_cache_version != default_raw_cache_version + assert alt_doc_cache_version != default_doc_cache_version + + _assert_caches_empty(ls_alt) + + _ = ls_alt.request_document_symbols(main_cpp) + _assert_caches_modified(ls_alt) From 3345153cce7e11cabc45d7cd62eaa124f803a65a Mon Sep 17 00:00:00 2001 From: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:53:54 +0200 Subject: [PATCH 4/5] Clean up CodeQL workflow by removing languages Removed unused languages from CodeQL workflow matrix. --- .github/workflows/codeql.yml | 103 +++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..718cbd4e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,103 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '38 16 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" From d32c053c55c8216803cad21fbd81e2ce52f4b0e9 Mon Sep 17 00:00:00 2001 From: Koushik-Salammagari <138836560+Koushik-Salammagari@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:19:23 -0700 Subject: [PATCH 5/5] Fix Lean4 stale cache: skip caching empty document symbol responses (#1356) * Fix Lean4 stale cache: skip caching empty document symbol responses When the Lean language server is queried before `lake build` completes, it returns empty symbol lists. Previously these were written to the persistent cache, permanently hiding symbols even after the build finished. Now only non-empty responses are cached. Adds a unit test that verifies empty responses from the LSP layer are never written to _raw_document_symbols_cache. Fixes #1217 * fix: run poe format and add changelog entry for #1356 Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> --- CHANGELOG.md | 1 + src/solidlsp/ls.py | 9 ++++++--- test/solidlsp/lean4/test_lean4_basic.py | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf145fe..64349290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Language Servers: - Fix: clangd capability checks now tolerate valid initialize response shape differences and invalidate cached C++ document symbols when clangd/compile commands context changes #1359 - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files + - Fix: Lean4 stale cache — empty document symbol responses (returned before `lake build` completes) are no longer persisted, preventing symbols from being permanently hidden #1356 # v1.1.2 (2026-04-14) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 4a1f5986..5b679ce9 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1331,9 +1331,12 @@ class SolidLanguageServer(ABC): {"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}} ) - # update cache - self._raw_document_symbols_cache[cache_key] = (fd.content_hash, response) - self._raw_document_symbols_cache_is_modified = True + # Only cache non-empty results. An empty or None response can occur when the language server + # has not yet finished indexing or building the project (e.g. Lean 4 before `lake build`), + # and caching it would permanently serve stale data even after the project is ready. + if response: + self._raw_document_symbols_cache[cache_key] = (fd.content_hash, response) + self._raw_document_symbols_cache_is_modified = True return response diff --git a/test/solidlsp/lean4/test_lean4_basic.py b/test/solidlsp/lean4/test_lean4_basic.py index b8f35efa..a20a32cd 100644 --- a/test/solidlsp/lean4/test_lean4_basic.py +++ b/test/solidlsp/lean4/test_lean4_basic.py @@ -12,6 +12,8 @@ Test Repository Structure: - Main.lean: Main entry point using Helper, plus multiply and calculate functions """ +from unittest.mock import patch + import pytest from solidlsp.ls import SolidLanguageServer @@ -146,6 +148,29 @@ class TestLean4LanguageServer: assert def_location["uri"].endswith("Main.lean"), f"Expected definition in Main.lean, got: {def_location['uri']}" assert def_location["range"]["start"]["line"] == 5, f"Expected definition at line 5, got: {def_location['range']['start']['line']}" + @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) + def test_empty_symbol_response_not_cached(self, language_server: SolidLanguageServer) -> None: + """ + Test that an empty document symbol response is not written to the cache. + + This guards against the scenario where the Lean language server returns no symbols + before `lake build` has completed. If the empty result were cached, subsequent + requests after the build would still return empty symbols even though the LS now + has the full build artifacts available. + """ + file_path = "Helper.lean" + + # Patch the LSP server to return an empty list, simulating a pre-build response. + with patch.object(language_server.server.send, "document_symbol", return_value=[]): + result = language_server._request_document_symbols(file_path, None) + assert result == [], "Expected the (mocked) empty response to be returned as-is" + + # The empty result must NOT have been written to the cache. + assert file_path not in language_server._raw_document_symbols_cache, ( + "Empty symbol response must not be stored in the cache; " + "it would permanently hide symbols that become available after `lake build`" + ) + @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_go_to_definition_across_files(self, language_server: SolidLanguageServer) -> None: """