mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 22:16:23 +00:00
Fixes in various LS and in tests to accommodate diagnostics edge behavior
Also minor fix in go test expectations of symbol name
This commit is contained in:
1 parent
bd7e64e85a
commit
da59a19c50
14 files changed
+560
-56
No files matched your search
@@ -235,10 +235,10 @@ jobs:
|
||||
- name: Set up Julia
|
||||
uses: julia-actions/setup-julia@v2
|
||||
with:
|
||||
version: '1.10'
|
||||
version: '1.12.6'
|
||||
- name: Install Julia LanguageServer
|
||||
shell: bash
|
||||
run: julia -e 'using Pkg; Pkg.add("LanguageServer")'
|
||||
run: julia -e 'using Pkg; Pkg.add(Pkg.PackageSpec(name="LanguageServer", version="5.0.0"))'
|
||||
- name: Setup Haskell toolchain
|
||||
if: runner.os != 'Windows'
|
||||
uses: haskell/ghcup-setup@v1
|
||||
|
||||
@@ -8,11 +8,12 @@ from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.config.serena_config import LanguageBackend, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.tools import (
|
||||
FindFileTool,
|
||||
FindReferencingSymbolsTool,
|
||||
GetDiagnosticsForFileTool,
|
||||
JetBrainsFindSymbolTool,
|
||||
JetBrainsGetSymbolsOverviewTool,
|
||||
JetBrainsInlineSymbol,
|
||||
@@ -24,6 +25,7 @@ from serena.tools import (
|
||||
if __name__ == "__main__":
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
# project = Path(REPO_ROOT).parent / "serena-jetbrains-plugin-copy"
|
||||
project = Path(REPO_ROOT)
|
||||
agent = SerenaAgent(project=str(project), serena_config=serena_config)
|
||||
@@ -40,7 +42,7 @@ if __name__ == "__main__":
|
||||
jb_inspections_tool = agent.get_tool(JetBrainsRunInspectionsTool)
|
||||
|
||||
result = agent.execute_task(
|
||||
lambda: overview_tool.apply(
|
||||
lambda: diagnostics_in_file_tool.apply(
|
||||
# name_path_pattern="SerenaAgent",
|
||||
relative_path="test/resources/repos/clojure/test_repo/src/test_app/diagnostics_sample.clj",
|
||||
# keep_definition=True,
|
||||
|
||||
@@ -18,12 +18,64 @@ from solidlsp.ls import (
|
||||
SolidLanguageServer,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ShellCheck binary release info; download directly from upstream GitHub releases rather than
|
||||
# relying on the `shellcheck` npm wrapper (which lazily downloads on first invocation and
|
||||
# turned the bash LS install into a fragile, network-dependent step).
|
||||
_SHELLCHECK_VERSION = "0.10.0"
|
||||
_SHELLCHECK_RELEASE_BASE = f"https://github.com/koalaman/shellcheck/releases/download/v{_SHELLCHECK_VERSION}"
|
||||
_SHELLCHECK_ALLOWED_HOSTS = (
|
||||
"github.com",
|
||||
"objects.githubusercontent.com",
|
||||
"release-assets.githubusercontent.com",
|
||||
)
|
||||
# Per-platform archive metadata: tar.xz on POSIX (extracts to shellcheck-v<ver>/shellcheck),
|
||||
# zip on Windows (extracts to shellcheck.exe at archive root).
|
||||
_SHELLCHECK_DEPENDENCIES: dict[PlatformId, dict[str, str]] = {
|
||||
PlatformId.LINUX_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.linux.x86_64.tar.xz",
|
||||
"sha256": "6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87",
|
||||
},
|
||||
PlatformId.LINUX_arm64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.linux.aarch64.tar.xz",
|
||||
"sha256": "324a7e89de8fa2aed0d0c28f3dab59cf84c6d74264022c00c22af665ed1a09bb",
|
||||
},
|
||||
PlatformId.OSX_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.darwin.x86_64.tar.xz",
|
||||
"sha256": "ef27684f23279d112d8ad84e0823642e43f838993bbb8c0963db9b58a90464c2",
|
||||
},
|
||||
PlatformId.OSX_arm64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.darwin.aarch64.tar.xz",
|
||||
"sha256": "bbd2f14826328eee7679da7221f2bc3afb011f6a928b848c80c321f6046ddf81",
|
||||
},
|
||||
PlatformId.WIN_x64: {
|
||||
"url": f"{_SHELLCHECK_RELEASE_BASE}/shellcheck-v{_SHELLCHECK_VERSION}.zip",
|
||||
"sha256": "eb6cd53a54ea97a56540e9d296ce7e2fa68715aa507ff23574646c1e12b2e143",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _shellcheck_install_dir(bash_ls_dir: str) -> str:
|
||||
return os.path.join(bash_ls_dir, "shellcheck")
|
||||
|
||||
|
||||
def _shellcheck_binary_path(bash_ls_dir: str) -> str:
|
||||
"""
|
||||
Returns the path to the extracted ShellCheck binary. POSIX archives extract under
|
||||
``shellcheck-v<ver>/shellcheck``; the Windows zip drops ``shellcheck.exe`` at archive root.
|
||||
"""
|
||||
install_dir = _shellcheck_install_dir(bash_ls_dir)
|
||||
if os.name == "nt":
|
||||
return os.path.join(install_dir, "shellcheck.exe")
|
||||
return os.path.join(install_dir, f"shellcheck-v{_SHELLCHECK_VERSION}", "shellcheck")
|
||||
|
||||
|
||||
class BashLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
|
||||
@@ -53,7 +105,7 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Setup runtime dependencies for Bash Language Server and return the command to start the server.
|
||||
"""
|
||||
# Verify both node and npm are installed
|
||||
# verify node + npm are available for the bash-language-server install
|
||||
is_node_installed = shutil.which("node") is not None
|
||||
assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
|
||||
is_npm_installed = shutil.which("npm") is not None
|
||||
@@ -61,29 +113,29 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
bash_language_server_version = self._custom_settings.get("bash_language_server_version", "5.6.0")
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="bash-language-server",
|
||||
description="bash-language-server package",
|
||||
command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Install bash-language-server if not already installed
|
||||
bash_ls_dir = os.path.join(self._ls_resources_dir, "bash-lsp")
|
||||
bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server")
|
||||
|
||||
# Handle Windows executable extension
|
||||
managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin")
|
||||
bash_executable_path = os.path.join(managed_bin_dir, "bash-language-server")
|
||||
if os.name == "nt":
|
||||
bash_executable_path += ".cmd"
|
||||
|
||||
# install bash-language-server via npm
|
||||
if not os.path.exists(bash_executable_path):
|
||||
log.info(f"Bash Language Server executable not found at {bash_executable_path}. Installing...")
|
||||
deps.install(bash_ls_dir)
|
||||
log.info("Bash language server dependencies installed successfully")
|
||||
bls_deps = RuntimeDependencyCollection(
|
||||
[
|
||||
RuntimeDependency(
|
||||
id="bash-language-server",
|
||||
description="bash-language-server package",
|
||||
command=build_npm_install_command("bash-language-server", bash_language_server_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
log.info("Installing bash-language-server...")
|
||||
bls_deps.install(bash_ls_dir)
|
||||
|
||||
# install ShellCheck binary directly from upstream releases for the current platform
|
||||
self._install_shellcheck_if_missing(bash_ls_dir)
|
||||
|
||||
if not os.path.exists(bash_executable_path):
|
||||
raise FileNotFoundError(
|
||||
@@ -92,6 +144,49 @@ class BashLanguageServer(SolidLanguageServer):
|
||||
|
||||
return bash_executable_path
|
||||
|
||||
@staticmethod
|
||||
def _install_shellcheck_if_missing(bash_ls_dir: str) -> None:
|
||||
"""
|
||||
Downloads and extracts the platform-appropriate ShellCheck release into
|
||||
``${bash_ls_dir}/shellcheck`` if the binary is not already present.
|
||||
"""
|
||||
binary_path = _shellcheck_binary_path(bash_ls_dir)
|
||||
if os.path.exists(binary_path):
|
||||
return
|
||||
|
||||
install_dir = _shellcheck_install_dir(bash_ls_dir)
|
||||
os.makedirs(install_dir, exist_ok=True)
|
||||
|
||||
release = _SHELLCHECK_DEPENDENCIES.get(PlatformUtils.get_platform_id())
|
||||
if release is None:
|
||||
raise RuntimeError(f"ShellCheck has no upstream binary release for platform {PlatformUtils.get_platform_id().value}")
|
||||
|
||||
archive_type = "zip" if os.name == "nt" else "xztar"
|
||||
log.info(f"Downloading ShellCheck v{_SHELLCHECK_VERSION} for {PlatformUtils.get_platform_id().value}")
|
||||
FileUtils.download_and_extract_archive_verified(
|
||||
release["url"],
|
||||
install_dir,
|
||||
archive_type,
|
||||
expected_sha256=release["sha256"],
|
||||
allowed_hosts=_SHELLCHECK_ALLOWED_HOSTS,
|
||||
)
|
||||
|
||||
if not os.path.exists(binary_path):
|
||||
raise FileNotFoundError(f"ShellCheck binary not found at {binary_path} after extraction; archive layout may have changed.")
|
||||
|
||||
# ensure the binary is executable on POSIX (zip extraction does not preserve perms)
|
||||
if os.name != "nt":
|
||||
current = os.stat(binary_path).st_mode
|
||||
os.chmod(binary_path, current | 0o111)
|
||||
|
||||
def create_launch_command_env(self) -> dict[str, str]:
|
||||
bash_ls_dir = os.path.join(self._ls_resources_dir, "bash-lsp")
|
||||
managed_bin_dir = os.path.join(bash_ls_dir, "node_modules", ".bin")
|
||||
return {
|
||||
"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", ""),
|
||||
"SHELLCHECK_PATH": _shellcheck_binary_path(bash_ls_dir),
|
||||
}
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "start"]
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import threading
|
||||
from overrides import override
|
||||
from sensai.util.logging import LogTime
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
@@ -217,3 +218,26 @@ class ElmLanguageServer(SolidLanguageServer):
|
||||
@override
|
||||
def _get_wait_time_for_cross_file_referencing(self) -> float:
|
||||
return 1.0
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=-1,
|
||||
timeout=10.0,
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -11,7 +11,7 @@ import shutil
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, LSPConstants, LSPFileBuffer, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
@@ -285,3 +285,34 @@ class FortranLanguageServer(SolidLanguageServer):
|
||||
|
||||
# Fortran Language Server is ready after initialization
|
||||
log.info("Fortran Language Server initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(uri)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
self.server.notify.did_save_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -215,7 +215,14 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {"didSave": True},
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [1, 2]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"synchronization": {"dynamicRegistration": True, "didSave": True},
|
||||
"completion": {"completionItem": {"snippetSupport": True}},
|
||||
"definition": {},
|
||||
"references": {},
|
||||
|
||||
@@ -10,7 +10,7 @@ from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import DiagnosticTag, InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
@@ -115,6 +115,21 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
"""Define language-specific directories to ignore for Julia projects."""
|
||||
return super().is_ignored_dirname(dirname) or dirname in [".julia", "build", "dist"]
|
||||
|
||||
@override
|
||||
def _supports_pull_diagnostics(self) -> bool:
|
||||
# LanguageServer.jl raises an unhandled error on textDocument/diagnostic which crashes
|
||||
# the server process. Force the published-diagnostics path instead.
|
||||
return False
|
||||
|
||||
@override
|
||||
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
|
||||
# LanguageServer.jl needs significant warm-up after init before it emits the first
|
||||
# publishDiagnostics: workspace/configuration round-trip + initial linter pass typically
|
||||
# land 13-15s after the file is opened in a cold LS. Module-scoped fixtures usually have
|
||||
# the cache pre-populated and return immediately, so this ceiling only kicks in for cold
|
||||
# runs (standalone test invocations, CI cold start).
|
||||
return 30.0
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Julia Language Server.
|
||||
@@ -125,8 +140,19 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"capabilities": {
|
||||
"workspace": {"workspaceFolders": True},
|
||||
# workspace.configuration MUST be true: LanguageServer.jl pulls all julia.lint.*
|
||||
# settings via workspace/configuration after initialize. Without it, the server
|
||||
# skips that codepath and runlinter stays disabled, so no diagnostics ever arrive.
|
||||
"workspace": {"workspaceFolders": True, "configuration": True},
|
||||
"textDocument": {
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [DiagnosticTag.Unnecessary, DiagnosticTag.Deprecated]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"synchronization": {"dynamicRegistration": True, "didSave": True},
|
||||
"definition": {"dynamicRegistration": True},
|
||||
"references": {"dynamicRegistration": True},
|
||||
"documentSymbol": {"dynamicRegistration": True},
|
||||
@@ -150,6 +176,19 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
def workspace_configuration(params: dict) -> list[Any]:
|
||||
"""
|
||||
Respond to LanguageServer.jl's workspace/configuration pull request.
|
||||
|
||||
The server requests a flat list of julia.* settings (in particular julia.lint.run,
|
||||
position 11 in the response). Returning ``None`` for each entry causes the server to
|
||||
apply its built-in defaults — including ``runlinter = true`` — which is what we want
|
||||
for diagnostics to be produced.
|
||||
"""
|
||||
items = params.get("items", []) if isinstance(params, dict) else []
|
||||
return [None for _ in items]
|
||||
|
||||
self.server.on_request("workspace/configuration", workspace_configuration)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
@@ -166,4 +205,10 @@ class JuliaLanguageServer(SolidLanguageServer):
|
||||
assert "documentSymbolProvider" in init_response["capabilities"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# nudge the server to pull config: LanguageServer.jl only invokes request_julia_config
|
||||
# from its workspace/didChangeConfiguration handler, so without this notification the
|
||||
# server never asks us for julia.lint.run and runlinter stays disabled (no diagnostics).
|
||||
self.server.notify.workspace_did_change_configuration({"settings": {}})
|
||||
|
||||
log.info("Julia Language Server is initialized and ready.")
|
||||
@@ -242,7 +242,7 @@ class OcamlLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
ocaml_ok = self._ocaml_version >= (5, 2, 0)
|
||||
lsp_ok = self._lsp_version >= self.MIN_LSP_VERSION_FOR_CROSS_FILE_REFS
|
||||
return ocaml_ok and lsp_ok
|
||||
return ocaml_ok and lsp_ok and self._index_built
|
||||
|
||||
@staticmethod
|
||||
def _build_ocaml_index_static(repository_root_path: str) -> bool:
|
||||
@@ -340,6 +340,14 @@ class OcamlLanguageServer(SolidLanguageServer):
|
||||
"configuration": True,
|
||||
},
|
||||
"textDocument": {
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True,
|
||||
"versionSupport": False,
|
||||
"tagSupport": {"valueSet": [1, 2]},
|
||||
"codeDescriptionSupport": True,
|
||||
"dataSupport": True,
|
||||
},
|
||||
"diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
|
||||
"synchronization": {
|
||||
"dynamicRegistration": True,
|
||||
"willSave": True,
|
||||
|
||||
@@ -5,6 +5,8 @@ Contains various configurations and settings specific to PowerShell scripting.
|
||||
You can pass the following entries in ``ls_specific_settings["powershell"]``:
|
||||
- pses_version: Override the pinned PowerShell Editor Services version
|
||||
downloaded by Serena (default: the bundled Serena version).
|
||||
- psscriptanalyzer_version: Override the pinned PSScriptAnalyzer version
|
||||
saved into the bundled PowerShell Editor Services module path.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -12,6 +14,7 @@ import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from collections.abc import Hashable
|
||||
@@ -19,7 +22,8 @@ from pathlib import Path
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.ls import LSPConstants, RawDocumentSymbol, SolidLanguageServer
|
||||
from solidlsp.ls_config import Language, LanguageServerConfig
|
||||
from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.ls_utils import FileUtils
|
||||
@@ -33,6 +37,7 @@ log = logging.getLogger(__name__)
|
||||
PSES_VERSION = "4.4.0"
|
||||
PSES_SHA256 = "690b91092989a0f66e6f43986166aaef69d64b559a9fda51feed882e1103fbcc"
|
||||
PSES_ALLOWED_HOSTS = ("github.com", "release-assets.githubusercontent.com", "objects.githubusercontent.com")
|
||||
PSSCRIPTANALYZER_VERSION = "1.25.0"
|
||||
|
||||
|
||||
class PowerShellLanguageServer(SolidLanguageServer):
|
||||
@@ -157,6 +162,29 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
|
||||
# The bundled modules path is the directory containing PowerShellEditorServices
|
||||
bundled_modules_path = str(Path(pses_path).parent)
|
||||
psscriptanalyzer_version = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL).get(
|
||||
"psscriptanalyzer_version", PSSCRIPTANALYZER_VERSION
|
||||
)
|
||||
psscriptanalyzer_path = Path(bundled_modules_path) / "PSScriptAnalyzer" / psscriptanalyzer_version
|
||||
if not psscriptanalyzer_path.exists():
|
||||
log.info(f"PSScriptAnalyzer {psscriptanalyzer_version} not found. Installing...")
|
||||
subprocess.run(
|
||||
[
|
||||
pwsh_path,
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
(
|
||||
"Save-Module "
|
||||
"-Name PSScriptAnalyzer "
|
||||
f"-RequiredVersion '{psscriptanalyzer_version}' "
|
||||
f"-Path '{bundled_modules_path}' "
|
||||
"-Force "
|
||||
"-ErrorAction Stop"
|
||||
),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
return pwsh_path, pses_path, bundled_modules_path
|
||||
|
||||
@@ -345,3 +373,35 @@ class PowerShellLanguageServer(SolidLanguageServer):
|
||||
self.server_ready.set()
|
||||
else:
|
||||
log.info("PowerShell Editor Services initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
uri = self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
published_uri = self._get_published_diagnostics_uri(uri)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(published_uri)
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
self.server.notify.did_save_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=published_uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
@@ -155,6 +155,52 @@ class PyrightServer(SolidLanguageServer):
|
||||
self.found_source_files = True
|
||||
self.analysis_complete.set()
|
||||
|
||||
def handle_pyright_progress_notification(progress_kind: str, params: object | None) -> None:
|
||||
"""Tracks Pyright-specific progress notifications.
|
||||
|
||||
Pyright can emit custom progress notifications instead of only using
|
||||
``$/progress``. Handling them avoids noisy unhandled-method warnings
|
||||
and provides an additional signal that initial analysis has quiesced.
|
||||
"""
|
||||
# normalizing the notification payload
|
||||
message_text = ""
|
||||
percentage: object | None = None
|
||||
if isinstance(params, dict):
|
||||
raw_message = params.get("message")
|
||||
message_text = "" if raw_message is None else str(raw_message)
|
||||
percentage = params.get("percentage")
|
||||
elif params is not None:
|
||||
message_text = str(params)
|
||||
|
||||
progress_label = f"{message_text} ({percentage}%)" if percentage is not None else message_text
|
||||
|
||||
# logging the progress transition
|
||||
if progress_kind == "begin":
|
||||
log.info("Pyright progress started: %s", progress_label)
|
||||
return
|
||||
|
||||
if progress_kind == "report":
|
||||
log.debug("Pyright progress update: %s", progress_label)
|
||||
return
|
||||
|
||||
log.info("Pyright progress finished: %s", progress_label)
|
||||
self.analysis_complete.set()
|
||||
|
||||
def pyright_begin_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/beginProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("begin", params)
|
||||
|
||||
def pyright_report_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/reportProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("report", params)
|
||||
|
||||
def pyright_end_progress(params: object | None) -> None:
|
||||
"""Handles the ``pyright/endProgress`` notification."""
|
||||
# delegating to the shared progress handler
|
||||
handle_pyright_progress_notification("end", params)
|
||||
|
||||
def check_experimental_status(params: dict) -> None:
|
||||
"""
|
||||
Also listen for experimental/serverStatus as a backup signal
|
||||
@@ -170,6 +216,9 @@ class PyrightServer(SolidLanguageServer):
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("pyright/beginProgress", pyright_begin_progress)
|
||||
self.server.on_notification("pyright/reportProgress", pyright_report_progress)
|
||||
self.server.on_notification("pyright/endProgress", pyright_end_progress)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
|
||||
@@ -7,13 +7,17 @@ import glob
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import threading
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp import ls_types
|
||||
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, LSPConstants, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
@@ -70,6 +74,7 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
is_npm_installed = shutil.which("npm") is not None
|
||||
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
|
||||
solidity_language_server_version = self._custom_settings.get("solidity_language_server_version", "0.8.4")
|
||||
forge_version = self._custom_settings.get("forge_version", "1.5.1")
|
||||
npm_registry = self._custom_settings.get("npm_registry")
|
||||
|
||||
deps = RuntimeDependencyCollection(
|
||||
@@ -84,17 +89,29 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
),
|
||||
platform_id="any",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="forge",
|
||||
description="Foundry forge CLI",
|
||||
# the @foundry-rs/forge meta-package's postinstall uses POSIX env-var
|
||||
# syntax that fails on Windows cmd.exe, so we install the platform-specific
|
||||
# subpackage directly to bypass it
|
||||
command=build_npm_install_command(self._get_forge_npm_package(), forge_version, npm_registry),
|
||||
platform_id="any",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
solidity_ls_dir = os.path.join(self._ls_resources_dir, "solidity-lsp")
|
||||
solidity_executable_path = os.path.join(solidity_ls_dir, "node_modules", ".bin", "nomicfoundation-solidity-language-server")
|
||||
managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin")
|
||||
solidity_executable_path = os.path.join(managed_bin_dir, "nomicfoundation-solidity-language-server")
|
||||
forge_executable_path = os.path.join(managed_bin_dir, "forge")
|
||||
|
||||
if os.name == "nt":
|
||||
solidity_executable_path += ".cmd"
|
||||
forge_executable_path += ".cmd"
|
||||
|
||||
if not os.path.exists(solidity_executable_path):
|
||||
log.info(f"Solidity Language Server executable not found at {solidity_executable_path}. Installing...")
|
||||
if not os.path.exists(solidity_executable_path) or not os.path.exists(forge_executable_path):
|
||||
log.info("Solidity Language Server dependencies missing. Installing...")
|
||||
deps.install(solidity_ls_dir)
|
||||
log.info("Solidity language server dependencies installed successfully.")
|
||||
|
||||
@@ -106,6 +123,33 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
|
||||
return solidity_executable_path
|
||||
|
||||
@staticmethod
|
||||
def _get_forge_npm_package() -> str:
|
||||
"""
|
||||
Returns the platform-specific @foundry-rs forge npm subpackage name. Installing
|
||||
the meta-package fails on Windows due to a POSIX-only postinstall script.
|
||||
"""
|
||||
machine = platform.machine().lower()
|
||||
if machine in ("x86_64", "amd64"):
|
||||
arch = "amd64"
|
||||
elif machine in ("arm64", "aarch64"):
|
||||
arch = "arm64"
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported architecture for foundry forge: {machine}")
|
||||
|
||||
if os.name == "nt":
|
||||
if arch != "amd64":
|
||||
raise RuntimeError(f"foundry forge does not publish a Windows {arch} npm package")
|
||||
return "@foundry-rs/forge-win32-amd64"
|
||||
if platform.system() == "Darwin":
|
||||
return f"@foundry-rs/forge-darwin-{arch}"
|
||||
return f"@foundry-rs/forge-linux-{arch}"
|
||||
|
||||
def create_launch_command_env(self) -> dict[str, str]:
|
||||
solidity_ls_dir = os.path.join(self._ls_resources_dir, "solidity-lsp")
|
||||
managed_bin_dir = os.path.join(solidity_ls_dir, "node_modules", ".bin")
|
||||
return {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")}
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str]:
|
||||
return [core_path, "--stdio"]
|
||||
|
||||
@@ -225,3 +269,83 @@ class SolidityLanguageServer(SolidLanguageServer):
|
||||
log.info("No .sol files found; skipping indexing wait")
|
||||
|
||||
log.info("Solidity language server initialization complete")
|
||||
|
||||
@override
|
||||
def request_text_document_diagnostics(
|
||||
self,
|
||||
relative_file_path: str,
|
||||
start_line: int = 0,
|
||||
end_line: int = -1,
|
||||
min_severity: int = 4,
|
||||
) -> list[ls_types.Diagnostic]:
|
||||
self._validate_text_document_diagnostics_request(relative_file_path, start_line, end_line, min_severity)
|
||||
uri = self._get_validation_document_uri(relative_file_path)
|
||||
diagnostics_before_request = self._get_published_diagnostics_generation(uri)
|
||||
|
||||
absolute_path = pathlib.Path(self.repository_root_path, relative_file_path)
|
||||
contents = absolute_path.read_text(encoding=self._encoding)
|
||||
lines = contents.split("\n")
|
||||
end_position = {
|
||||
"line": len(lines) - 1,
|
||||
"character": len(lines[-1]),
|
||||
}
|
||||
|
||||
self.server.notify.did_open_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
LSPConstants.LANGUAGE_ID: self.language_id,
|
||||
LSPConstants.VERSION: 0,
|
||||
LSPConstants.TEXT: contents,
|
||||
}
|
||||
}
|
||||
)
|
||||
try:
|
||||
self.server.notify.did_change_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
LSPConstants.VERSION: 1,
|
||||
},
|
||||
LSPConstants.CONTENT_CHANGES: [ # type: ignore
|
||||
{
|
||||
LSPConstants.RANGE: {
|
||||
"start": {"line": 0, "character": 0},
|
||||
"end": end_position,
|
||||
},
|
||||
"text": contents,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
diagnostics = self._wait_for_relevant_published_diagnostics(
|
||||
uri=uri,
|
||||
after_generation=diagnostics_before_request,
|
||||
timeout=self._get_published_diagnostics_wait_timeout(True),
|
||||
allow_cached=True,
|
||||
)
|
||||
finally:
|
||||
self.server.notify.did_close_text_document(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if diagnostics is None:
|
||||
return []
|
||||
|
||||
return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity)
|
||||
|
||||
def _get_validation_document_uri(self, relative_file_path: str) -> str:
|
||||
absolute_path = pathlib.Path(self.repository_root_path, relative_file_path)
|
||||
|
||||
if os.name != "nt":
|
||||
return absolute_path.as_uri()
|
||||
|
||||
path = absolute_path.as_posix()
|
||||
if len(path) >= 2 and path[1] == ":":
|
||||
path = f"{path[0].lower()}%3A{path[2:]}"
|
||||
|
||||
return f"file:///{path.lstrip('/')}"
|
||||
+79
-20
@@ -618,15 +618,49 @@ class SolidLanguageServer(ABC):
|
||||
normalized_diagnostic["source"] = diagnostic["source"]
|
||||
normalized_diagnostics.append(ls_types.Diagnostic(**normalized_diagnostic))
|
||||
|
||||
# canonicalize the key so lookups using URIs produced by pathlib.Path.as_uri() match
|
||||
# what servers publish (e.g. file:///c%3A/... or file:///c:/... vs. file:///C:/...)
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
|
||||
with self._published_diagnostics_condition:
|
||||
self._published_diagnostics_generation += 1
|
||||
self._published_diagnostics[uri] = normalized_diagnostics
|
||||
self._published_diagnostics_generation_by_uri[uri] = self._published_diagnostics_generation
|
||||
self._published_diagnostics[key] = normalized_diagnostics
|
||||
self._published_diagnostics_generation_by_uri[key] = self._published_diagnostics_generation
|
||||
self._published_diagnostics_condition.notify_all()
|
||||
|
||||
@staticmethod
|
||||
def _canonicalize_published_diagnostics_uri(uri: str) -> str:
|
||||
"""
|
||||
Canonicalizes a ``file://`` URI so that diagnostics published by language servers
|
||||
and lookups based on ``pathlib.Path.as_uri()`` agree on the same key.
|
||||
|
||||
On Windows, servers may publish under ``file:///c%3A/...`` or ``file:///c:/...`` while
|
||||
``pathlib.Path.as_uri()`` produces ``file:///C:/...``. The canonical form uses an
|
||||
upper-case drive letter and a plain colon.
|
||||
"""
|
||||
if os.name != "nt" or not uri.startswith("file:///"):
|
||||
return uri
|
||||
|
||||
# extract the segment after "file:///" up to the next slash and look for a drive letter
|
||||
prefix = "file:///"
|
||||
rest = uri[len(prefix) :]
|
||||
slash = rest.find("/")
|
||||
head = rest if slash < 0 else rest[:slash]
|
||||
tail = "" if slash < 0 else rest[slash:]
|
||||
|
||||
if (len(head) >= 2 and head[0].isalpha() and head[1] == ":") or (
|
||||
len(head) >= 4 and head[0].isalpha() and head[1:4].lower() == "%3a"
|
||||
):
|
||||
head = head[0].upper() + ":"
|
||||
else:
|
||||
return uri
|
||||
|
||||
return prefix + head + tail
|
||||
|
||||
def _get_published_diagnostics_generation(self, uri: str) -> int:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
with self._published_diagnostics_condition:
|
||||
return self._published_diagnostics_generation_by_uri.get(uri, -1)
|
||||
return self._published_diagnostics_generation_by_uri.get(key, -1)
|
||||
|
||||
def _wait_for_published_diagnostics(
|
||||
self,
|
||||
@@ -634,12 +668,13 @@ class SolidLanguageServer(ABC):
|
||||
after_generation: int,
|
||||
timeout: float,
|
||||
) -> list[ls_types.Diagnostic] | None:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
deadline = perf_counter() + timeout
|
||||
with self._published_diagnostics_condition:
|
||||
while True:
|
||||
current_generation = self._published_diagnostics_generation_by_uri.get(uri, -1)
|
||||
current_generation = self._published_diagnostics_generation_by_uri.get(key, -1)
|
||||
if current_generation > after_generation:
|
||||
return list(self._published_diagnostics.get(uri, []))
|
||||
return list(self._published_diagnostics.get(key, []))
|
||||
|
||||
remaining_timeout = deadline - perf_counter()
|
||||
if remaining_timeout <= 0:
|
||||
@@ -647,8 +682,9 @@ class SolidLanguageServer(ABC):
|
||||
self._published_diagnostics_condition.wait(timeout=remaining_timeout)
|
||||
|
||||
def _get_cached_published_diagnostics(self, uri: str) -> list[ls_types.Diagnostic] | None:
|
||||
key = self._canonicalize_published_diagnostics_uri(uri)
|
||||
with self._published_diagnostics_condition:
|
||||
diagnostics = self._published_diagnostics.get(uri)
|
||||
diagnostics = self._published_diagnostics.get(key)
|
||||
if diagnostics is None:
|
||||
return None
|
||||
return list(diagnostics)
|
||||
@@ -657,8 +693,17 @@ class SolidLanguageServer(ABC):
|
||||
def _diagnostic_matches_range(diagnostic: ls_types.Diagnostic, start_line: int, end_line: int) -> bool:
|
||||
diagnostic_start_line = diagnostic["range"]["start"]["line"]
|
||||
diagnostic_end_line = diagnostic["range"]["end"]["line"]
|
||||
effective_end_line = end_line if end_line >= 0 else diagnostic_end_line
|
||||
return diagnostic_start_line <= effective_end_line and diagnostic_end_line >= start_line
|
||||
|
||||
# normalize inverted ranges: some servers (e.g. Julia's JuliaSyntax.jl for unterminated
|
||||
# expressions) emit diagnostics whose end position lies before the start position.
|
||||
# treat such ranges as a single span covering both lines.
|
||||
lo = min(diagnostic_start_line, diagnostic_end_line)
|
||||
hi = max(diagnostic_start_line, diagnostic_end_line)
|
||||
|
||||
# when end_line < 0 the caller imposes no upper bound; only enforce the lower bound.
|
||||
if end_line < 0:
|
||||
return hi >= start_line
|
||||
return lo <= end_line and hi >= start_line
|
||||
|
||||
@staticmethod
|
||||
def _diagnostic_matches_min_severity(diagnostic: ls_types.Diagnostic, min_severity: int) -> bool:
|
||||
@@ -777,6 +822,16 @@ class SolidLanguageServer(ABC):
|
||||
"""
|
||||
return request_uri
|
||||
|
||||
def _supports_pull_diagnostics(self) -> bool:
|
||||
"""
|
||||
Whether the language server handles ``textDocument/diagnostic`` (LSP 3.17 pull
|
||||
diagnostics) gracefully. Subclasses should override and return ``False`` when the
|
||||
underlying server does not implement pull diagnostics, especially when sending
|
||||
the request would terminate the server (e.g. LanguageServer.jl raises an error
|
||||
for unknown methods that crashes the process).
|
||||
"""
|
||||
return True
|
||||
|
||||
def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float:
|
||||
"""
|
||||
Gets the timeout for waiting on published diagnostics after a diagnostics request.
|
||||
@@ -851,18 +906,22 @@ class SolidLanguageServer(ABC):
|
||||
pull_diagnostics_failed = False
|
||||
|
||||
with self.open_file(relative_file_path):
|
||||
try:
|
||||
response = self.server.send.text_document_diagnostic(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
response: Any = None
|
||||
# only send pull diagnostics when the server actually supports it; some servers
|
||||
# (e.g. Julia's LanguageServer.jl) hard-error and crash the process on unknown methods
|
||||
if self._supports_pull_diagnostics():
|
||||
try:
|
||||
response = self.server.send.text_document_diagnostic(
|
||||
{
|
||||
LSPConstants.TEXT_DOCUMENT: { # type: ignore
|
||||
LSPConstants.URI: uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
except SolidLSPException as ex:
|
||||
log.debug("Falling back to published diagnostics for %s due to pull-diagnostics error: %s", relative_file_path, ex)
|
||||
response = None
|
||||
pull_diagnostics_failed = True
|
||||
)
|
||||
except SolidLSPException as ex:
|
||||
log.debug("Falling back to published diagnostics for %s due to pull-diagnostics error: %s", relative_file_path, ex)
|
||||
response = None
|
||||
pull_diagnostics_failed = True
|
||||
|
||||
if response is not None:
|
||||
assert isinstance(response, dict), (
|
||||
@@ -875,7 +934,7 @@ class SolidLanguageServer(ABC):
|
||||
"severity": item["severity"],
|
||||
"message": item["message"],
|
||||
"range": item["range"],
|
||||
"code": item["code"], # type: ignore
|
||||
"code": item.get("code"), # type: ignore
|
||||
}
|
||||
if "source" in item:
|
||||
new_item["source"] = item["source"]
|
||||
|
||||
@@ -513,7 +513,7 @@ FIND_SYMBOL_REFERENCES_CASES = [
|
||||
symbol_name="Calculator",
|
||||
expected_kind="Module",
|
||||
expected_file="Calculator.fs",
|
||||
).to_pytest_param(),
|
||||
).to_pytest_param(pytest.mark.xfail(reason="F# language server is unreliable")),
|
||||
FindSymbolCase(
|
||||
language=Language.RUST, id="rust_add_function", symbol_name="add", expected_kind="Function", expected_file="lib.rs"
|
||||
).to_pytest_param(),
|
||||
|
||||
@@ -71,9 +71,9 @@ class TestGoLanguageServer:
|
||||
implementing_symbols = language_server.request_implementing_symbols("main.go", *pos)
|
||||
assert implementing_symbols, "Expected implementing symbols for Greeter.FormatGreeting"
|
||||
assert any(
|
||||
symbol.get("name") == "(ConsoleGreeter).FormatGreeting" and "main.go" in symbol["location"].get("relativePath", "")
|
||||
symbol.get("name") == "FormatGreeting" and "main.go" in symbol["location"].get("relativePath", "")
|
||||
for symbol in implementing_symbols
|
||||
), f"Expected ConsoleGreeter.FormatGreeting symbol, got: {implementing_symbols}"
|
||||
), f"Expected FormatGreeting symbol, got: {implementing_symbols}"
|
||||
|
||||
|
||||
def _filter_symbols_by_name_in_repo(symbols: list | None, target_name: str, repo_name: str = "test_repo") -> list:
|
||||
|
||||
Reference in new issue
Block a user