Merge branch 'main' of github.com:oraios/serena

This commit is contained in:
Michael Panchenko committed 2026-04-17 09:30:42 +02:00
commit b47ee4c2f1
9 files changed
+322 -15

No files matched your search

+103
View File
@@ -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}}"
+7
View File
@@ -13,6 +13,13 @@ 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: 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)
* General:
+1 -1
View File
@@ -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",
@@ -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
@@ -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
+6 -3
View File
@@ -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
+71
View File
@@ -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)
+25
View File
@@ -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:
"""
Generated
+4 -4
View File
@@ -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" },