From bd7e64e85a04751f629a2e4094575e0b6e67aa37 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Tue, 28 Apr 2026 22:06:41 +0200 Subject: [PATCH] Fixed diagnostics files uri handling, added low-level tests --- .gitignore | 4 +- .serena/project.yml | 1 + scripts/demo_run_tools.py | 6 +- .../jetbrains/jetbrains_plugin_client.py | 2 +- .../language_servers/elm_language_server.py | 28 +++++-- .../fortran_language_server.py | 1 + .../language_servers/lean4_language_server.py | 1 + .../language_servers/pascal_server.py | 9 +++ .../solidity_language_server.py | 1 + .../typescript_language_server.py | 26 +++++- .../language_servers/vue_language_server.py | 13 +++ src/solidlsp/language_servers/zls.py | 1 + src/solidlsp/ls.py | 79 ++++++++++++++++--- .../bash/test_repo/diagnostics_sample.sh | 4 + .../test_repo/src/diagnostics_sample.cr | 5 ++ .../test_repo/lib/diagnostics_sample.dart | 3 + .../test_repo/lib/diagnostics_sample.ex | 10 +++ .../repos/elm/test_repo/DiagnosticsSample.elm | 5 ++ .../test_repo/src/diagnostics_sample.erl | 5 ++ .../fortran/test_repo/diagnostics_sample.f90 | 5 ++ .../com/example/DiagnosticsSample.groovy | 13 +++ .../test_repo/src/DiagnosticsSample.hs | 7 ++ .../haxe/test_repo/src/DiagnosticsSample.hx | 10 +++ .../hlsl/test_repo/diagnostics_sample.hlsl | 3 + .../json/test_repo/diagnostics_sample.json | 4 + .../julia/test_repo/diagnostics_sample.jl | 3 + .../lean4/test_repo/DiagnosticsSample.lean | 5 -- .../matlab/test_repo/diagnostics_sample.m | 3 + .../nix/test_repo/diagnostics_sample.nix | 4 + .../ocaml/test_repo/lib/diagnostics_sample.ml | 1 + test/resources/repos/ocaml/test_repo/lib/dune | 3 +- .../pascal/test_repo/diagnostics_sample.pas | 4 + .../perl/test_repo/diagnostics_sample.pl | 5 ++ .../test_repo/diagnostics_sample.ps1 | 9 +-- .../scala/com/example/DiagnosticsSample.scala | 9 +++ .../test_repo/contracts/DiagnosticsSample.sol | 7 ++ .../repos/swift/test_repo/Package.swift | 5 +- .../test_repo/src/diagnostics_sample.swift | 3 + .../test_repo/diagnostics_sample.sv | 5 ++ .../terraform/test_repo/diagnostics_sample.tf | 4 + .../toml/test_repo/diagnostics_sample.toml | 2 + .../vue/test_repo/src/DiagnosticsSample.vue | 3 + .../yaml/test_repo/diagnostics_sample.yaml | 5 ++ test/resources/repos/zig/test_repo/build.zig | 2 +- .../zig/test_repo/src/diagnostics_sample.zig | 3 + .../repos/zig/test_repo/src/main.zig | 2 +- .../__snapshots__/test_symbol_editing.ambr | 26 +++--- test/solidlsp/bash/test_bash_diagnostics.py | 17 ++++ test/solidlsp/clojure/__init__.py | 4 +- .../clojure/test_clojure_diagnostics.py | 19 +++++ test/solidlsp/conftest.py | 9 +++ test/solidlsp/cpp/test_cpp_diagnostics.py | 17 ++++ .../crystal/test_crystal_diagnostics.py | 23 ++++++ .../csharp/test_csharp_diagnostics.py | 17 ++++ test/solidlsp/dart/test_dart_diagnostics.py | 17 ++++ test/solidlsp/elixir/test_elixir_basic.py | 10 +++ test/solidlsp/elm/test_elm_diagnostics.py | 20 +++++ test/solidlsp/erlang/test_erlang_basic.py | 10 +++ .../fortran/test_fortran_diagnostics.py | 17 ++++ test/solidlsp/fsharp/test_fsharp_basic.py | 23 ++---- test/solidlsp/go/test_go_diagnostics.py | 17 ++++ test/solidlsp/groovy/test_groovy_basic.py | 10 +++ test/solidlsp/haskell/test_haskell_basic.py | 10 +++ test/solidlsp/haxe/test_haxe_diagnostics.py | 17 ++++ test/solidlsp/hlsl/test_hlsl_diagnostics.py | 17 ++++ test/solidlsp/java/test_java_diagnostics.py | 17 ++++ .../solidlsp/json_ls/test_json_diagnostics.py | 17 ++++ test/solidlsp/julia/test_julia_basic.py | 10 +++ .../kotlin/test_kotlin_diagnostics.py | 17 ++++ test/solidlsp/lean4/test_lean4_diagnostics.py | 17 ++++ test/solidlsp/matlab/test_matlab_basic.py | 10 +++ test/solidlsp/nix/test_nix_basic.py | 10 +++ test/solidlsp/ocaml/test_ocaml_diagnostics.py | 17 ++++ .../pascal/test_pascal_diagnostics.py | 25 ++++++ test/solidlsp/perl/test_perl_basic.py | 10 +++ test/solidlsp/php/test_php_diagnostics.py | 17 ++++ .../powershell/test_powershell_diagnostics.py | 17 ++++ .../python/test_python_diagnostics.py | 19 +++++ test/solidlsp/rust/test_rust_basic.py | 2 +- test/solidlsp/rust/test_rust_diagnostics.py | 17 ++++ .../scala/test_scala_language_server.py | 10 +++ .../solidity/test_solidity_diagnostics.py | 24 ++++++ test/solidlsp/swift/test_swift_basic.py | 10 +++ .../test_systemverilog_diagnostics.py | 17 ++++ .../terraform/test_terraform_diagnostics.py | 17 ++++ test/solidlsp/toml/test_toml_diagnostics.py | 17 ++++ .../typescript/test_typescript_diagnostics.py | 17 ++++ test/solidlsp/util/diagnostics.py | 19 +++++ test/solidlsp/vue/test_vue_diagnostics.py | 17 ++++ .../solidlsp/yaml_ls/test_yaml_diagnostics.py | 17 ++++ test/solidlsp/zig/test_zig_basic.py | 10 +++ 91 files changed, 958 insertions(+), 72 deletions(-) create mode 100644 test/resources/repos/bash/test_repo/diagnostics_sample.sh create mode 100644 test/resources/repos/crystal/test_repo/src/diagnostics_sample.cr create mode 100644 test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart create mode 100644 test/resources/repos/elixir/test_repo/lib/diagnostics_sample.ex create mode 100644 test/resources/repos/elm/test_repo/DiagnosticsSample.elm create mode 100644 test/resources/repos/erlang/test_repo/src/diagnostics_sample.erl create mode 100644 test/resources/repos/fortran/test_repo/diagnostics_sample.f90 create mode 100644 test/resources/repos/groovy/test_repo/src/main/groovy/com/example/DiagnosticsSample.groovy create mode 100644 test/resources/repos/haskell/test_repo/src/DiagnosticsSample.hs create mode 100644 test/resources/repos/haxe/test_repo/src/DiagnosticsSample.hx create mode 100644 test/resources/repos/hlsl/test_repo/diagnostics_sample.hlsl create mode 100644 test/resources/repos/json/test_repo/diagnostics_sample.json create mode 100644 test/resources/repos/julia/test_repo/diagnostics_sample.jl create mode 100644 test/resources/repos/matlab/test_repo/diagnostics_sample.m create mode 100644 test/resources/repos/nix/test_repo/diagnostics_sample.nix create mode 100644 test/resources/repos/ocaml/test_repo/lib/diagnostics_sample.ml create mode 100644 test/resources/repos/pascal/test_repo/diagnostics_sample.pas create mode 100644 test/resources/repos/perl/test_repo/diagnostics_sample.pl create mode 100644 test/resources/repos/scala/test_repo/src/main/scala/com/example/DiagnosticsSample.scala create mode 100644 test/resources/repos/solidity/test_repo/contracts/DiagnosticsSample.sol create mode 100644 test/resources/repos/swift/test_repo/src/diagnostics_sample.swift create mode 100644 test/resources/repos/systemverilog/test_repo/diagnostics_sample.sv create mode 100644 test/resources/repos/terraform/test_repo/diagnostics_sample.tf create mode 100644 test/resources/repos/toml/test_repo/diagnostics_sample.toml create mode 100644 test/resources/repos/vue/test_repo/src/DiagnosticsSample.vue create mode 100644 test/resources/repos/yaml/test_repo/diagnostics_sample.yaml create mode 100644 test/resources/repos/zig/test_repo/src/diagnostics_sample.zig create mode 100644 test/solidlsp/bash/test_bash_diagnostics.py create mode 100644 test/solidlsp/clojure/test_clojure_diagnostics.py create mode 100644 test/solidlsp/cpp/test_cpp_diagnostics.py create mode 100644 test/solidlsp/crystal/test_crystal_diagnostics.py create mode 100644 test/solidlsp/csharp/test_csharp_diagnostics.py create mode 100644 test/solidlsp/dart/test_dart_diagnostics.py create mode 100644 test/solidlsp/elm/test_elm_diagnostics.py create mode 100644 test/solidlsp/fortran/test_fortran_diagnostics.py create mode 100644 test/solidlsp/go/test_go_diagnostics.py create mode 100644 test/solidlsp/haxe/test_haxe_diagnostics.py create mode 100644 test/solidlsp/hlsl/test_hlsl_diagnostics.py create mode 100644 test/solidlsp/java/test_java_diagnostics.py create mode 100644 test/solidlsp/json_ls/test_json_diagnostics.py create mode 100644 test/solidlsp/kotlin/test_kotlin_diagnostics.py create mode 100644 test/solidlsp/lean4/test_lean4_diagnostics.py create mode 100644 test/solidlsp/ocaml/test_ocaml_diagnostics.py create mode 100644 test/solidlsp/pascal/test_pascal_diagnostics.py create mode 100644 test/solidlsp/php/test_php_diagnostics.py create mode 100644 test/solidlsp/powershell/test_powershell_diagnostics.py create mode 100644 test/solidlsp/python/test_python_diagnostics.py create mode 100644 test/solidlsp/rust/test_rust_diagnostics.py create mode 100644 test/solidlsp/solidity/test_solidity_diagnostics.py create mode 100644 test/solidlsp/systemverilog/test_systemverilog_diagnostics.py create mode 100644 test/solidlsp/terraform/test_terraform_diagnostics.py create mode 100644 test/solidlsp/toml/test_toml_diagnostics.py create mode 100644 test/solidlsp/typescript/test_typescript_diagnostics.py create mode 100644 test/solidlsp/util/diagnostics.py create mode 100644 test/solidlsp/vue/test_vue_diagnostics.py create mode 100644 test/solidlsp/yaml_ls/test_yaml_diagnostics.py diff --git a/.gitignore b/.gitignore index dbc94920..37c1649c 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,8 @@ downloads/ eggs/ .eggs/ lib/ +!test/resources/repos/dart/test_repo/lib/ +!test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart lib64/ parts/ sdist/ @@ -271,4 +273,4 @@ cabal.project.local* zz-misc/ vue-implementation/ -news/news.json \ No newline at end of file +news/news.json diff --git a/.serena/project.yml b/.serena/project.yml index 84b6fcf8..fbaead68 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -28,6 +28,7 @@ project_name: "serena" languages: - python - typescript +- clojure # whether to use project's .gitignore files to ignore files ignore_all_files_in_gitignore: true diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index b0ed063b..4914201c 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -16,6 +16,7 @@ from serena.tools import ( JetBrainsFindSymbolTool, JetBrainsGetSymbolsOverviewTool, JetBrainsInlineSymbol, + JetBrainsRunInspectionsTool, JetBrainsSafeDeleteTool, SearchForPatternTool, ) @@ -35,12 +36,13 @@ if __name__ == "__main__": overview_tool = agent.get_tool(JetBrainsGetSymbolsOverviewTool) safe_delete_tool = agent.get_tool(JetBrainsSafeDeleteTool) inline_symbol = agent.get_tool(JetBrainsInlineSymbol) + diagnostics_in_file_tool = agent.get_tool(GetDiagnosticsForFileTool) + jb_inspections_tool = agent.get_tool(JetBrainsRunInspectionsTool) result = agent.execute_task( lambda: overview_tool.apply( # name_path_pattern="SerenaAgent", - relative_path="src/serena/agent.py", - depth=2, + relative_path="test/resources/repos/clojure/test_repo/src/test_app/diagnostics_sample.clj", # keep_definition=True, ) ) diff --git a/src/serena/jetbrains/jetbrains_plugin_client.py b/src/serena/jetbrains/jetbrains_plugin_client.py index c662ec76..c7b440ce 100644 --- a/src/serena/jetbrains/jetbrains_plugin_client.py +++ b/src/serena/jetbrains/jetbrains_plugin_client.py @@ -689,7 +689,7 @@ class JetBrainsPluginClient(ToStringMixin): request_data["startLine"] = start_line if end_line is not None: request_data["endLine"] = end_line - return cast(jb.RunInspectionsResponse, self._make_request("POST", "/runInspections", request_data)) + return cast(jb.RunInspectionsResponse, self._make_request("POST", "/runInspectionsOnFile", request_data)) def list_inspections( self, diff --git a/src/solidlsp/language_servers/elm_language_server.py b/src/solidlsp/language_servers/elm_language_server.py index e1960c32..0c6139c1 100644 --- a/src/solidlsp/language_servers/elm_language_server.py +++ b/src/solidlsp/language_servers/elm_language_server.py @@ -32,9 +32,10 @@ class ElmLanguageServer(SolidLanguageServer): Creates an ElmLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. """ elm_lsp_executable_path = self._setup_runtime_dependencies(config, solidlsp_settings) + managed_bin_dir = os.path.join(self.ls_resources_dir(solidlsp_settings), "elm-lsp", "node_modules", ".bin") # Resolve ELM_HOME to absolute path if it's set to a relative path - env = {} + env = {"PATH": managed_bin_dir + os.pathsep + os.environ.get("PATH", "")} elm_home = os.environ.get("ELM_HOME") if elm_home: if not os.path.isabs(elm_home): @@ -65,9 +66,15 @@ class ElmLanguageServer(SolidLanguageServer): """ Setup runtime dependencies for Elm Language Server and return the command to start the server. """ - # Check if elm-language-server is already installed globally + elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM) + elm_language_server_version = elm_config.get("elm_language_server_version", "2.8.0") + elm_compiler_version = elm_config.get("elm_compiler_version", "0.19.1-6") + npm_registry = elm_config.get("npm_registry") + + # Check if elm-language-server and elm are already installed globally system_elm_ls = shutil.which("elm-language-server") - if system_elm_ls: + system_elm = shutil.which("elm") + if system_elm_ls and system_elm: log.info(f"Found system-installed elm-language-server at {system_elm_ls}") return [system_elm_ls, "--stdio"] @@ -76,10 +83,6 @@ class ElmLanguageServer(SolidLanguageServer): 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 assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." - elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM) - elm_language_server_version = elm_config.get("elm_language_server_version", "2.8.0") - npm_registry = elm_config.get("npm_registry") - deps = RuntimeDependencyCollection( [ RuntimeDependency( @@ -88,6 +91,12 @@ class ElmLanguageServer(SolidLanguageServer): command=build_npm_install_command("@elm-tooling/elm-language-server", elm_language_server_version, npm_registry), platform_id="any", ), + RuntimeDependency( + id="elm", + description="Elm compiler package", + command=build_npm_install_command("elm", elm_compiler_version, npm_registry), + platform_id="any", + ), ] ) @@ -98,6 +107,10 @@ class ElmLanguageServer(SolidLanguageServer): log.info(f"Elm Language Server executable not found at {elm_ls_executable_path}. Installing...") with LogTime("Installation of Elm language server dependencies", logger=log): deps.install(elm_ls_dir) + elif not system_elm: + log.info("Elm compiler not found on PATH. Installing a managed Elm compiler...") + with LogTime("Installation of Elm compiler dependency", logger=log): + deps.install(elm_ls_dir) if not os.path.exists(elm_ls_executable_path): raise FileNotFoundError( @@ -128,6 +141,7 @@ class ElmLanguageServer(SolidLanguageServer): "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, "codeAction": {"dynamicRegistration": True}, "rename": {"dynamicRegistration": True}, + "publishDiagnostics": {"relatedInformation": True}, }, "workspace": { "workspaceFolders": True, diff --git a/src/solidlsp/language_servers/fortran_language_server.py b/src/solidlsp/language_servers/fortran_language_server.py index 2cd6b099..a1d5d56d 100644 --- a/src/solidlsp/language_servers/fortran_language_server.py +++ b/src/solidlsp/language_servers/fortran_language_server.py @@ -220,6 +220,7 @@ class FortranLanguageServer(SolidLanguageServer): "formatting": {"dynamicRegistration": True}, "rangeFormatting": {"dynamicRegistration": True}, "codeAction": {"dynamicRegistration": True}, + "publishDiagnostics": {"relatedInformation": True}, }, "workspace": { "workspaceFolders": True, diff --git a/src/solidlsp/language_servers/lean4_language_server.py b/src/solidlsp/language_servers/lean4_language_server.py index 463e4992..961a65b6 100644 --- a/src/solidlsp/language_servers/lean4_language_server.py +++ b/src/solidlsp/language_servers/lean4_language_server.py @@ -124,6 +124,7 @@ class Lean4LanguageServer(SolidLanguageServer): "documentationFormat": ["markdown", "plaintext"], }, }, + "publishDiagnostics": {"relatedInformation": True}, }, "workspace": { "workspaceFolders": True, diff --git a/src/solidlsp/language_servers/pascal_server.py b/src/solidlsp/language_servers/pascal_server.py index 9de2a926..1f2b7de7 100644 --- a/src/solidlsp/language_servers/pascal_server.py +++ b/src/solidlsp/language_servers/pascal_server.py @@ -777,6 +777,14 @@ class PascalLanguageServer(SolidLanguageServer): if value: initialization_options[var] = value + initialization_options.update( + { + "checkSyntax": True, + "publishDiagnostics": True, + "showSyntaxErrors": True, + } + ) + initialize_params = { "locale": "en", "capabilities": { @@ -831,6 +839,7 @@ class PascalLanguageServer(SolidLanguageServer): }, "formatting": {"dynamicRegistration": True}, "rangeFormatting": {"dynamicRegistration": True}, + "publishDiagnostics": {"relatedInformation": True}, }, "workspace": { "workspaceFolders": True, diff --git a/src/solidlsp/language_servers/solidity_language_server.py b/src/solidlsp/language_servers/solidity_language_server.py index 223a6683..1e7ccd2e 100644 --- a/src/solidlsp/language_servers/solidity_language_server.py +++ b/src/solidlsp/language_servers/solidity_language_server.py @@ -148,6 +148,7 @@ class SolidityLanguageServer(SolidLanguageServer): "dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"], # type: ignore[list-item] }, + "publishDiagnostics": {"relatedInformation": True}, }, "workspace": { "workspaceFolders": True, diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index 32e9ac22..8b1c04e8 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -89,7 +89,10 @@ class TypeScriptLanguageServer(SolidLanguageServer): self.server_ready = threading.Event() self.initialize_searcher_command_available = threading.Event() - # Progress tracking for $/progress notifications (project indexing, etc.) + # tracking asynchronous diagnostics publication + self._published_diagnostics_timeout = 5.0 + + # tracking project indexing progress self._progress_lock = threading.Lock() self._active_progress_tokens: set[str] = set() self._indexing_complete = threading.Event() @@ -243,9 +246,11 @@ class TypeScriptLanguageServer(SolidLanguageServer): "signatureHelp": {"dynamicRegistration": True}, "codeAction": {"dynamicRegistration": True}, "rename": {"dynamicRegistration": True, "prepareSupport": True}, + "publishDiagnostics": {"relatedInformation": True}, }, "workspace": { "workspaceFolders": True, + "configuration": True, "didChangeConfiguration": {"dynamicRegistration": True}, "symbol": {"dynamicRegistration": True}, }, @@ -293,6 +298,10 @@ class TypeScriptLanguageServer(SolidLanguageServer): def execute_client_command_handler(params: dict) -> list: return [] + def configuration_handler(params: dict) -> list: + items = params.get("items", []) + return [{} for _ in items] + def do_nothing(params: dict) -> None: return @@ -352,6 +361,7 @@ class TypeScriptLanguageServer(SolidLanguageServer): self._indexing_complete.set() self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_request("workspace/configuration", configuration_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_request("window/workDoneProgress/create", work_done_progress_create) @@ -397,6 +407,20 @@ class TypeScriptLanguageServer(SolidLanguageServer): self.INDEXING_PROGRESS_TIMEOUT, ) + @override + def _get_published_diagnostics_uri(self, request_uri: str) -> str: + if os.name != "nt" or not request_uri.startswith("file:///"): + return request_uri + + path_part = request_uri[len("file:///") :] + if len(path_part) >= 2 and path_part[0].isalpha() and path_part[1] == ":": + return f"file:///{path_part[0].lower()}%3A{path_part[2:]}" + return request_uri + + @override + def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float: + return self._published_diagnostics_timeout + @override def _get_wait_time_for_cross_file_referencing(self) -> float: return 2 diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py index 06123623..f99056ae 100644 --- a/src/solidlsp/language_servers/vue_language_server.py +++ b/src/solidlsp/language_servers/vue_language_server.py @@ -426,6 +426,18 @@ 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) + @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._ensure_ls_operational() + assert self._ts_server is not None + return self._ts_server.request_text_document_diagnostics(relative_file_path, start_line, end_line, min_severity) + 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. @@ -598,6 +610,7 @@ class VueLanguageServer(SolidLanguageServer): "signatureHelp": {"dynamicRegistration": True}, "codeAction": {"dynamicRegistration": True}, "rename": {"dynamicRegistration": True, "prepareSupport": True}, + "publishDiagnostics": {"relatedInformation": True}, }, "workspace": { "workspaceFolders": True, diff --git a/src/solidlsp/language_servers/zls.py b/src/solidlsp/language_servers/zls.py index 368f57eb..cb5652fc 100644 --- a/src/solidlsp/language_servers/zls.py +++ b/src/solidlsp/language_servers/zls.py @@ -132,6 +132,7 @@ class ZigLanguageServer(SolidLanguageServer): "dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"], }, + "publishDiagnostics": {"relatedInformation": True}, }, "workspace": { "workspaceFolders": True, diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 546c6a9f..4ea04694 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -13,7 +13,7 @@ from collections.abc import Hashable, Iterator from contextlib import contextmanager from copy import copy from pathlib import Path, PurePath -from time import perf_counter, sleep +from time import monotonic, perf_counter, sleep from typing import Any, Self, Union, cast import pathspec @@ -754,21 +754,77 @@ class SolidLanguageServer(ABC): :return: the published diagnostics, or ``None`` if no diagnostics are available. """ 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: list[ls_types.Diagnostic] | None = None # keeping the document open with self.open_file(relative_file_path): - diagnostics = self._wait_for_published_diagnostics(uri=uri, after_generation=after_generation, timeout=timeout) - - # falling back to cached diagnostics - if diagnostics is None and allow_cached: - diagnostics = self._get_cached_published_diagnostics(uri) + diagnostics = self._wait_for_relevant_published_diagnostics( + uri=published_uri, + after_generation=after_generation, + timeout=timeout, + allow_cached=allow_cached, + ) if diagnostics is None: return None return self._filter_diagnostics(diagnostics, start_line, end_line, min_severity) + def _get_published_diagnostics_uri(self, request_uri: str) -> str: + """ + Gets the URI under which published diagnostics should be looked up. + """ + return request_uri + + def _get_published_diagnostics_wait_timeout(self, pull_diagnostics_failed: bool) -> float: + """ + Gets the timeout for waiting on published diagnostics after a diagnostics request. + """ + return 2.5 + + def _accept_published_diagnostics(self, diagnostics: list[ls_types.Diagnostic]) -> bool: + """ + Determines whether a published diagnostics payload should satisfy the current wait. + """ + return bool(diagnostics) + + def _wait_for_relevant_published_diagnostics( + self, + uri: str, + after_generation: int, + timeout: float, + allow_cached: bool = True, + ) -> list[ls_types.Diagnostic] | None: + """ + Waits for a published diagnostics payload that is relevant for the current request. + """ + deadline = monotonic() + timeout + current_after_generation = after_generation + + while True: + remaining_timeout = deadline - monotonic() + if remaining_timeout <= 0: + break + + diagnostics = self._wait_for_published_diagnostics( + uri=uri, + after_generation=current_after_generation, + timeout=remaining_timeout, + ) + if diagnostics is None: + break + if self._accept_published_diagnostics(diagnostics): + return diagnostics + current_after_generation = self._get_published_diagnostics_generation(uri) + + if allow_cached: + diagnostics = self._get_cached_published_diagnostics(uri) + if diagnostics is not None and self._accept_published_diagnostics(diagnostics): + return diagnostics + + return None + def request_text_document_diagnostics( self, relative_file_path: str, @@ -789,7 +845,8 @@ class SolidLanguageServer(ABC): :return: A list of diagnostics for the file """ 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) + published_uri = self._get_published_diagnostics_uri(uri) + diagnostics_before_request = self._get_published_diagnostics_generation(published_uri) ret: list[ls_types.Diagnostic] | None = None pull_diagnostics_failed = False @@ -825,13 +882,11 @@ class SolidLanguageServer(ABC): ret.append(ls_types.Diagnostic(**new_item)) if not ret: - published_diagnostics = self._wait_for_published_diagnostics( - uri=uri, + published_diagnostics = self._wait_for_relevant_published_diagnostics( + uri=published_uri, after_generation=diagnostics_before_request, - timeout=2.5 if pull_diagnostics_failed else 0.5, + timeout=self._get_published_diagnostics_wait_timeout(pull_diagnostics_failed), ) - if published_diagnostics is None: - published_diagnostics = self._get_cached_published_diagnostics(uri) if published_diagnostics is not None: ret = published_diagnostics diff --git a/test/resources/repos/bash/test_repo/diagnostics_sample.sh b/test/resources/repos/bash/test_repo/diagnostics_sample.sh new file mode 100644 index 00000000..050a8a99 --- /dev/null +++ b/test/resources/repos/bash/test_repo/diagnostics_sample.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +if then + echo "$missingGreeting" +fi diff --git a/test/resources/repos/crystal/test_repo/src/diagnostics_sample.cr b/test/resources/repos/crystal/test_repo/src/diagnostics_sample.cr new file mode 100644 index 00000000..b408a202 --- /dev/null +++ b/test/resources/repos/crystal/test_repo/src/diagnostics_sample.cr @@ -0,0 +1,5 @@ +def broken_factory : String + missingGreeting +end + +broken_consumer = missingConsumerValue diff --git a/test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart b/test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart new file mode 100644 index 00000000..d309d9fc --- /dev/null +++ b/test/resources/repos/dart/test_repo/lib/diagnostics_sample.dart @@ -0,0 +1,3 @@ +String brokenFactory() { + return 'broken' +} diff --git a/test/resources/repos/elixir/test_repo/lib/diagnostics_sample.ex b/test/resources/repos/elixir/test_repo/lib/diagnostics_sample.ex new file mode 100644 index 00000000..af510437 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/diagnostics_sample.ex @@ -0,0 +1,10 @@ +defmodule DiagnosticsSample do + def broken_factory do + missing_greeting + end + + def broken_consumer do + value = broken_factory() + {value, missing_consumer_value} + end +end diff --git a/test/resources/repos/elm/test_repo/DiagnosticsSample.elm b/test/resources/repos/elm/test_repo/DiagnosticsSample.elm new file mode 100644 index 00000000..2880fa7b --- /dev/null +++ b/test/resources/repos/elm/test_repo/DiagnosticsSample.elm @@ -0,0 +1,5 @@ +module DiagnosticsSample exposing (brokenFactory) + +brokenFactory : String +brokenFactory = + "unterminated diff --git a/test/resources/repos/erlang/test_repo/src/diagnostics_sample.erl b/test/resources/repos/erlang/test_repo/src/diagnostics_sample.erl new file mode 100644 index 00000000..edb18776 --- /dev/null +++ b/test/resources/repos/erlang/test_repo/src/diagnostics_sample.erl @@ -0,0 +1,5 @@ +-module(diagnostics_sample). +-export([broken_factory/0, broken_consumer/0]). + +broken_factory() -> MissingGreeting. +broken_consumer() -> MissingConsumerValue. diff --git a/test/resources/repos/fortran/test_repo/diagnostics_sample.f90 b/test/resources/repos/fortran/test_repo/diagnostics_sample.f90 new file mode 100644 index 00000000..2c23e5df --- /dev/null +++ b/test/resources/repos/fortran/test_repo/diagnostics_sample.f90 @@ -0,0 +1,5 @@ +program diagnostics_sample + implicit none + integer :: duplicated_value + real :: duplicated_value +end program diagnostics_sample diff --git a/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/DiagnosticsSample.groovy b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/DiagnosticsSample.groovy new file mode 100644 index 00000000..a176203e --- /dev/null +++ b/test/resources/repos/groovy/test_repo/src/main/groovy/com/example/DiagnosticsSample.groovy @@ -0,0 +1,13 @@ +package com.example + +class DiagnosticsSample { + static String brokenFactory() { + missingGreeting + } + + static void brokenConsumer() { + def value = brokenFactory() + println(value) + println(missingConsumerValue) + } +} diff --git a/test/resources/repos/haskell/test_repo/src/DiagnosticsSample.hs b/test/resources/repos/haskell/test_repo/src/DiagnosticsSample.hs new file mode 100644 index 00000000..e5777b1f --- /dev/null +++ b/test/resources/repos/haskell/test_repo/src/DiagnosticsSample.hs @@ -0,0 +1,7 @@ +module DiagnosticsSample where + +brokenFactory :: String +brokenFactory = missingGreeting + +brokenConsumer :: String +brokenConsumer = missingConsumerValue diff --git a/test/resources/repos/haxe/test_repo/src/DiagnosticsSample.hx b/test/resources/repos/haxe/test_repo/src/DiagnosticsSample.hx new file mode 100644 index 00000000..3b2e2c29 --- /dev/null +++ b/test/resources/repos/haxe/test_repo/src/DiagnosticsSample.hx @@ -0,0 +1,10 @@ +class DiagnosticsSample { + static function brokenFactory():String { + return missingGreeting; + } + + static function brokenConsumer():Void { + trace(brokenFactory()); + trace(missingConsumerValue); + } +} diff --git a/test/resources/repos/hlsl/test_repo/diagnostics_sample.hlsl b/test/resources/repos/hlsl/test_repo/diagnostics_sample.hlsl new file mode 100644 index 00000000..ffc8075c --- /dev/null +++ b/test/resources/repos/hlsl/test_repo/diagnostics_sample.hlsl @@ -0,0 +1,3 @@ +float4 main() : SV_Target { + return missingGreeting; +} diff --git a/test/resources/repos/json/test_repo/diagnostics_sample.json b/test/resources/repos/json/test_repo/diagnostics_sample.json new file mode 100644 index 00000000..0cc75efd --- /dev/null +++ b/test/resources/repos/json/test_repo/diagnostics_sample.json @@ -0,0 +1,4 @@ +{ + "name": "diagnostics", + "value": +} diff --git a/test/resources/repos/julia/test_repo/diagnostics_sample.jl b/test/resources/repos/julia/test_repo/diagnostics_sample.jl new file mode 100644 index 00000000..ef7e4a51 --- /dev/null +++ b/test/resources/repos/julia/test_repo/diagnostics_sample.jl @@ -0,0 +1,3 @@ +function broken_factory( + value = 1 +end diff --git a/test/resources/repos/lean4/test_repo/DiagnosticsSample.lean b/test/resources/repos/lean4/test_repo/DiagnosticsSample.lean index a2c570ec..169754f7 100644 --- a/test/resources/repos/lean4/test_repo/DiagnosticsSample.lean +++ b/test/resources/repos/lean4/test_repo/DiagnosticsSample.lean @@ -1,6 +1 @@ def brokenFactory : Nat := - missingGreeting - -def brokenConsumer : Nat := - let value := brokenFactory - value + missingConsumerValue diff --git a/test/resources/repos/matlab/test_repo/diagnostics_sample.m b/test/resources/repos/matlab/test_repo/diagnostics_sample.m new file mode 100644 index 00000000..b0f87a62 --- /dev/null +++ b/test/resources/repos/matlab/test_repo/diagnostics_sample.m @@ -0,0 +1,3 @@ +function value = diagnostics_sample() +value = missingGreeting; +end diff --git a/test/resources/repos/nix/test_repo/diagnostics_sample.nix b/test/resources/repos/nix/test_repo/diagnostics_sample.nix new file mode 100644 index 00000000..320a6ab8 --- /dev/null +++ b/test/resources/repos/nix/test_repo/diagnostics_sample.nix @@ -0,0 +1,4 @@ +{ + greeting = missingGreeting; + consumer = missingConsumerValue; +} diff --git a/test/resources/repos/ocaml/test_repo/lib/diagnostics_sample.ml b/test/resources/repos/ocaml/test_repo/lib/diagnostics_sample.ml new file mode 100644 index 00000000..c23ccefe --- /dev/null +++ b/test/resources/repos/ocaml/test_repo/lib/diagnostics_sample.ml @@ -0,0 +1 @@ +let broken_factory ( diff --git a/test/resources/repos/ocaml/test_repo/lib/dune b/test/resources/repos/ocaml/test_repo/lib/dune index fe41978f..aba011b1 100644 --- a/test/resources/repos/ocaml/test_repo/lib/dune +++ b/test/resources/repos/ocaml/test_repo/lib/dune @@ -1,3 +1,4 @@ (library (public_name test_repo) - (name test_repo)) \ No newline at end of file + (name test_repo) + (modules test_repo)) diff --git a/test/resources/repos/pascal/test_repo/diagnostics_sample.pas b/test/resources/repos/pascal/test_repo/diagnostics_sample.pas new file mode 100644 index 00000000..7a3bf84b --- /dev/null +++ b/test/resources/repos/pascal/test_repo/diagnostics_sample.pas @@ -0,0 +1,4 @@ +program DiagnosticsSample; +begin + writeln( +end. diff --git a/test/resources/repos/perl/test_repo/diagnostics_sample.pl b/test/resources/repos/perl/test_repo/diagnostics_sample.pl new file mode 100644 index 00000000..4d78b686 --- /dev/null +++ b/test/resources/repos/perl/test_repo/diagnostics_sample.pl @@ -0,0 +1,5 @@ +use strict; +use warnings; + +print $missingGreeting; +print $missingConsumerValue; diff --git a/test/resources/repos/powershell/test_repo/diagnostics_sample.ps1 b/test/resources/repos/powershell/test_repo/diagnostics_sample.ps1 index d06165f2..c5ce7600 100644 --- a/test/resources/repos/powershell/test_repo/diagnostics_sample.ps1 +++ b/test/resources/repos/powershell/test_repo/diagnostics_sample.ps1 @@ -1,9 +1,4 @@ function Invoke-BrokenFactory { - Invoke-MissingGreeting -} - -function Invoke-BrokenConsumer { - $value = Invoke-BrokenFactory - Write-Output $value - Invoke-MissingConsumerValue + if ($true) { + Write-Output "broken" } diff --git a/test/resources/repos/scala/test_repo/src/main/scala/com/example/DiagnosticsSample.scala b/test/resources/repos/scala/test_repo/src/main/scala/com/example/DiagnosticsSample.scala new file mode 100644 index 00000000..ca9d929b --- /dev/null +++ b/test/resources/repos/scala/test_repo/src/main/scala/com/example/DiagnosticsSample.scala @@ -0,0 +1,9 @@ +package com.example + +object DiagnosticsSample { + def brokenFactory(): String = missingGreeting + def brokenConsumer(): Unit = { + println(brokenFactory()) + println(missingConsumerValue) + } +} diff --git a/test/resources/repos/solidity/test_repo/contracts/DiagnosticsSample.sol b/test/resources/repos/solidity/test_repo/contracts/DiagnosticsSample.sol new file mode 100644 index 00000000..7b2a0892 --- /dev/null +++ b/test/resources/repos/solidity/test_repo/contracts/DiagnosticsSample.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.0; + +contract DiagnosticsSample { + function brokenFactory() pure returns (string memory) { + return "broken"; + } +} diff --git a/test/resources/repos/swift/test_repo/Package.swift b/test/resources/repos/swift/test_repo/Package.swift index 13a02704..5e9b7cff 100644 --- a/test/resources/repos/swift/test_repo/Package.swift +++ b/test/resources/repos/swift/test_repo/Package.swift @@ -11,6 +11,7 @@ let package = Package( targets: [ .target( name: "test_repo", - dependencies: []), + dependencies: [], + path: "src"), ] -) \ No newline at end of file +) diff --git a/test/resources/repos/swift/test_repo/src/diagnostics_sample.swift b/test/resources/repos/swift/test_repo/src/diagnostics_sample.swift new file mode 100644 index 00000000..b70b2c68 --- /dev/null +++ b/test/resources/repos/swift/test_repo/src/diagnostics_sample.swift @@ -0,0 +1,3 @@ +func brokenFactory() -> String { + return +} diff --git a/test/resources/repos/systemverilog/test_repo/diagnostics_sample.sv b/test/resources/repos/systemverilog/test_repo/diagnostics_sample.sv new file mode 100644 index 00000000..b5364262 --- /dev/null +++ b/test/resources/repos/systemverilog/test_repo/diagnostics_sample.sv @@ -0,0 +1,5 @@ +module diagnostics_sample; + initial begin + if ( + end +endmodule diff --git a/test/resources/repos/terraform/test_repo/diagnostics_sample.tf b/test/resources/repos/terraform/test_repo/diagnostics_sample.tf new file mode 100644 index 00000000..15de22c0 --- /dev/null +++ b/test/resources/repos/terraform/test_repo/diagnostics_sample.tf @@ -0,0 +1,4 @@ +resource "aws_instance" "broken" { + ami = + instance_type = "t2.micro" +} diff --git a/test/resources/repos/toml/test_repo/diagnostics_sample.toml b/test/resources/repos/toml/test_repo/diagnostics_sample.toml new file mode 100644 index 00000000..ed4ae4c1 --- /dev/null +++ b/test/resources/repos/toml/test_repo/diagnostics_sample.toml @@ -0,0 +1,2 @@ +[diagnostics] +value = diff --git a/test/resources/repos/vue/test_repo/src/DiagnosticsSample.vue b/test/resources/repos/vue/test_repo/src/DiagnosticsSample.vue new file mode 100644 index 00000000..17bd62e3 --- /dev/null +++ b/test/resources/repos/vue/test_repo/src/DiagnosticsSample.vue @@ -0,0 +1,3 @@ + diff --git a/test/resources/repos/yaml/test_repo/diagnostics_sample.yaml b/test/resources/repos/yaml/test_repo/diagnostics_sample.yaml new file mode 100644 index 00000000..c07e469d --- /dev/null +++ b/test/resources/repos/yaml/test_repo/diagnostics_sample.yaml @@ -0,0 +1,5 @@ +name: diagnostics +value: + - good + - + broken: [ diff --git a/test/resources/repos/zig/test_repo/build.zig b/test/resources/repos/zig/test_repo/build.zig index da7317d0..c9c1ff5a 100644 --- a/test/resources/repos/zig/test_repo/build.zig +++ b/test/resources/repos/zig/test_repo/build.zig @@ -28,4 +28,4 @@ pub fn build(b: *std.Build) void { const run_lib_tests = b.addRunArtifact(lib_tests); const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_lib_tests.step); -} \ No newline at end of file +} diff --git a/test/resources/repos/zig/test_repo/src/diagnostics_sample.zig b/test/resources/repos/zig/test_repo/src/diagnostics_sample.zig new file mode 100644 index 00000000..844d6f14 --- /dev/null +++ b/test/resources/repos/zig/test_repo/src/diagnostics_sample.zig @@ -0,0 +1,3 @@ +pub fn brokenFactory() void { + const value = +} diff --git a/test/resources/repos/zig/test_repo/src/main.zig b/test/resources/repos/zig/test_repo/src/main.zig index 326b2cd0..156adabd 100644 --- a/test/resources/repos/zig/test_repo/src/main.zig +++ b/test/resources/repos/zig/test_repo/src/main.zig @@ -29,4 +29,4 @@ pub fn main() !void { pub fn greeting(name: []const u8) []const u8 { return std.fmt.allocPrint(std.heap.page_allocator, "Hello, {s}!", .{name}) catch "Hello!"; -} \ No newline at end of file +} diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index 8fd3a73b..ec23cf33 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -68,13 +68,13 @@ # name: test_delete_symbol[test_case1] ''' import { ConsoleGreeter, Greeter } from "./formatters"; - - + + export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); - + const greeter: Greeter = new ConsoleGreeter(); console.log(greeter.formatGreeting("World")); } @@ -521,7 +521,7 @@ # name: test_insert_in_rel_to_symbol[test_case1-after] ''' import { ConsoleGreeter, Greeter } from "./formatters"; - + export class DemoClass { value: number; constructor(value: number) { @@ -539,7 +539,7 @@ export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); - + const greeter: Greeter = new ConsoleGreeter(); console.log(greeter.formatGreeting("World")); } @@ -555,7 +555,7 @@ # name: test_insert_in_rel_to_symbol[test_case1-before] ''' import { ConsoleGreeter, Greeter } from "./formatters"; - + function newFunctionAfterClass(): void { console.log("This function is after DemoClass."); } @@ -573,7 +573,7 @@ export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); - + const greeter: Greeter = new ConsoleGreeter(); console.log(greeter.formatGreeting("World")); } @@ -589,7 +589,7 @@ # name: test_insert_in_rel_to_symbol[test_case2-after] ''' import { ConsoleGreeter, Greeter } from "./formatters"; - + export class DemoClass { value: number; constructor(value: number) { @@ -603,7 +603,7 @@ export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); - + const greeter: Greeter = new ConsoleGreeter(); console.log(greeter.formatGreeting("World")); } @@ -623,7 +623,7 @@ # name: test_insert_in_rel_to_symbol[test_case2-before] ''' import { ConsoleGreeter, Greeter } from "./formatters"; - + export class DemoClass { value: number; constructor(value: number) { @@ -641,7 +641,7 @@ export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); - + const greeter: Greeter = new ConsoleGreeter(); console.log(greeter.formatGreeting("World")); } @@ -1712,7 +1712,7 @@ # name: test_replace_body[test_case1] ''' import { ConsoleGreeter, Greeter } from "./formatters"; - + export class DemoClass { value: number; constructor(value: number) { @@ -1727,7 +1727,7 @@ export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); - + const greeter: Greeter = new ConsoleGreeter(); console.log(greeter.formatGreeting("World")); } diff --git a/test/solidlsp/bash/test_bash_diagnostics.py b/test/solidlsp/bash/test_bash_diagnostics.py new file mode 100644 index 00000000..bd1cc494 --- /dev/null +++ b/test/solidlsp/bash/test_bash_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.bash +class TestBashDiagnostics: + @pytest.mark.parametrize("language_server", [Language.BASH], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.sh", + (), + min_count=1, + ) diff --git a/test/solidlsp/clojure/__init__.py b/test/solidlsp/clojure/__init__.py index 5e8e1c93..f7d9dad6 100644 --- a/test/solidlsp/clojure/__init__.py +++ b/test/solidlsp/clojure/__init__.py @@ -13,8 +13,8 @@ def _test_clojure_cli() -> bool: CLI_FAIL = _test_clojure_cli() TEST_APP_PATH = Path("src") / "test_app" -CORE_PATH = str(TEST_APP_PATH / "core.clj").replace("\\", "/") -UTILS_PATH = str(TEST_APP_PATH / "utils.clj").replace("\\", "/") +CORE_PATH = str(TEST_APP_PATH / "core.clj") +UTILS_PATH = str(TEST_APP_PATH / "utils.clj") def is_clojure_cli_available() -> bool: diff --git a/test/solidlsp/clojure/test_clojure_diagnostics.py b/test/solidlsp/clojure/test_clojure_diagnostics.py new file mode 100644 index 00000000..a7bff96b --- /dev/null +++ b/test/solidlsp/clojure/test_clojure_diagnostics.py @@ -0,0 +1,19 @@ +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.clojure +class TestClojureDiagnostics: + @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + os.path.join("src", "test_app", "diagnostics_sample.clj"), + ("missing-greeting", "missing-consumer-value"), + min_count=2, + ) diff --git a/test/solidlsp/conftest.py b/test/solidlsp/conftest.py index 08bb6a01..ee563757 100644 --- a/test/solidlsp/conftest.py +++ b/test/solidlsp/conftest.py @@ -5,6 +5,12 @@ from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation PYTHON_BACKEND_LANGUAGES = [Language.PYTHON, Language.PYTHON_TY] +def is_diagnostics_test_file(relative_path: str) -> bool: + normalized_path = relative_path.replace("\\", "/") + filename = normalized_path.rsplit("/", 1)[-1].lower() + return filename.startswith(("diagnosticssample.", "diagnostics_sample.")) + + def has_malformed_name( symbol: UnifiedSymbolInformation, whitespace_allowed: bool = False, @@ -36,6 +42,9 @@ def request_all_symbols(language_server: SolidLanguageServer) -> list[UnifiedSym result: list[UnifiedSymbolInformation] = [] def visit(symbol: UnifiedSymbolInformation) -> None: + relative_path = symbol.get("location", {}).get("relativePath", "") + if relative_path and is_diagnostics_test_file(relative_path): + return result.append(symbol) for child in symbol.get("children", []): visit(child) diff --git a/test/solidlsp/cpp/test_cpp_diagnostics.py b/test/solidlsp/cpp/test_cpp_diagnostics.py new file mode 100644 index 00000000..20a58368 --- /dev/null +++ b/test/solidlsp/cpp/test_cpp_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.cpp +class TestCppDiagnostics: + @pytest.mark.parametrize("language_server", [Language.CPP], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.cpp", + (), + min_count=1, + ) diff --git a/test/solidlsp/crystal/test_crystal_diagnostics.py b/test/solidlsp/crystal/test_crystal_diagnostics.py new file mode 100644 index 00000000..e650adc1 --- /dev/null +++ b/test/solidlsp/crystal/test_crystal_diagnostics.py @@ -0,0 +1,23 @@ +import shutil + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + +pytestmark = [ + pytest.mark.crystal, + pytest.mark.skipif(shutil.which("crystalline") is None, reason="Crystalline is not installed"), +] + + +class TestCrystalDiagnostics: + @pytest.mark.parametrize("language_server", [Language.CRYSTAL], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/diagnostics_sample.cr", + (), + min_count=1, + ) diff --git a/test/solidlsp/csharp/test_csharp_diagnostics.py b/test/solidlsp/csharp/test_csharp_diagnostics.py new file mode 100644 index 00000000..de32fd1d --- /dev/null +++ b/test/solidlsp/csharp/test_csharp_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.csharp +class TestCsharpDiagnostics: + @pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "DiagnosticsSample.cs", + (), + min_count=1, + ) diff --git a/test/solidlsp/dart/test_dart_diagnostics.py b/test/solidlsp/dart/test_dart_diagnostics.py new file mode 100644 index 00000000..ddc5e398 --- /dev/null +++ b/test/solidlsp/dart/test_dart_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.dart +class TestDartDiagnostics: + @pytest.mark.parametrize("language_server", [Language.DART], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "lib/diagnostics_sample.dart", + (), + min_count=1, + ) diff --git a/test/solidlsp/elixir/test_elixir_basic.py b/test/solidlsp/elixir/test_elixir_basic.py index 11b983ca..7c44abc4 100644 --- a/test/solidlsp/elixir/test_elixir_basic.py +++ b/test/solidlsp/elixir/test_elixir_basic.py @@ -13,6 +13,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics from . import EXPERT_UNAVAILABLE, EXPERT_UNAVAILABLE_REASON @@ -138,3 +139,12 @@ class TestElixirBasic: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "lib/diagnostics_sample.ex", + (), + min_count=1, + ) diff --git a/test/solidlsp/elm/test_elm_diagnostics.py b/test/solidlsp/elm/test_elm_diagnostics.py new file mode 100644 index 00000000..744201a2 --- /dev/null +++ b/test/solidlsp/elm/test_elm_diagnostics.py @@ -0,0 +1,20 @@ +import shutil + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.elm +@pytest.mark.skipif(shutil.which("node") is None or shutil.which("npm") is None, reason="Elm diagnostics require Node.js and npm") +class TestElmDiagnostics: + @pytest.mark.parametrize("language_server", [Language.ELM], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "DiagnosticsSample.elm", + (), + min_count=1, + ) diff --git a/test/solidlsp/erlang/test_erlang_basic.py b/test/solidlsp/erlang/test_erlang_basic.py index a1dba06a..dc39535b 100644 --- a/test/solidlsp/erlang/test_erlang_basic.py +++ b/test/solidlsp/erlang/test_erlang_basic.py @@ -10,6 +10,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics from . import ERLANG_LS_UNAVAILABLE, ERLANG_LS_UNAVAILABLE_REASON @@ -55,3 +56,12 @@ class TestErlangLanguageServerBasics: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/diagnostics_sample.erl", + (), + min_count=1, + ) diff --git a/test/solidlsp/fortran/test_fortran_diagnostics.py b/test/solidlsp/fortran/test_fortran_diagnostics.py new file mode 100644 index 00000000..f607cd0b --- /dev/null +++ b/test/solidlsp/fortran/test_fortran_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.fortran +class TestFortranDiagnostics: + @pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.f90", + (), + min_count=1, + ) diff --git a/test/solidlsp/fsharp/test_fsharp_basic.py b/test/solidlsp/fsharp/test_fsharp_basic.py index f58ec0b1..f4f49ebc 100644 --- a/test/solidlsp/fsharp/test_fsharp_basic.py +++ b/test/solidlsp/fsharp/test_fsharp_basic.py @@ -9,6 +9,7 @@ from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils from test.conftest import find_identifier_position, get_repo_path, is_ci, language_has_verified_implementation_support from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics # Currently, most F# tests fail, there seems to be a regression or instability. @@ -196,21 +197,13 @@ class TestFSharpLanguageServer: assert isinstance(result["value"], list), "Completions should be a list" @pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True) - def test_diagnostics(self, language_server: SolidLanguageServer) -> None: - """Test getting diagnostics (errors, warnings) from F# files.""" - file_path = os.path.join("Program.fs") - - # FsAutoComplete uses publishDiagnostics notifications instead of textDocument/diagnostic requests - # So we'll test that the language server can handle files without crashing - # In real usage, diagnostics would come through the publishDiagnostics notification handler - - # Test that we can at least work with the file (open/close cycle) - with language_server.open_file(file_path) as _: - # If we can open and close the file without errors, basic diagnostics support is working - pass - - # This is a successful test - FsAutoComplete is working with F# files - assert True, "F# language server can handle files successfully" + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "DiagnosticsSample.fs", + (), + min_count=1, + ) @pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True) def test_bare_symbol_names(self, language_server) -> None: diff --git a/test/solidlsp/go/test_go_diagnostics.py b/test/solidlsp/go/test_go_diagnostics.py new file mode 100644 index 00000000..00b4cc48 --- /dev/null +++ b/test/solidlsp/go/test_go_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.go +class TestGoDiagnostics: + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.go", + ("missingGreeting", "missingConsumerValue"), + min_count=2, + ) diff --git a/test/solidlsp/groovy/test_groovy_basic.py b/test/solidlsp/groovy/test_groovy_basic.py index 47f22ed8..0637e3de 100644 --- a/test/solidlsp/groovy/test_groovy_basic.py +++ b/test/solidlsp/groovy/test_groovy_basic.py @@ -9,6 +9,7 @@ from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.ls_utils import SymbolUtils from solidlsp.settings import SolidLSPSettings from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics @pytest.mark.groovy @@ -117,3 +118,12 @@ class TestGroovyLanguageServer: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + def test_file_diagnostics(self) -> None: + assert self.language_server is not None + assert_file_diagnostics( + self.language_server, + "src/main/groovy/com/example/DiagnosticsSample.groovy", + (), + min_count=1, + ) diff --git a/test/solidlsp/haskell/test_haskell_basic.py b/test/solidlsp/haskell/test_haskell_basic.py index 67452151..5b23ad3d 100644 --- a/test/solidlsp/haskell/test_haskell_basic.py +++ b/test/solidlsp/haskell/test_haskell_basic.py @@ -21,6 +21,7 @@ from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics @pytest.mark.haskell @@ -240,3 +241,12 @@ class TestHaskellLanguageServer: for sym in malformed_symbols ] pytest.fail(f"Found malformed symbols: {diagnostics}", pytrace=False) + + @pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/DiagnosticsSample.hs", + (), + min_count=1, + ) diff --git a/test/solidlsp/haxe/test_haxe_diagnostics.py b/test/solidlsp/haxe/test_haxe_diagnostics.py new file mode 100644 index 00000000..2f789b60 --- /dev/null +++ b/test/solidlsp/haxe/test_haxe_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.haxe +class TestHaxeDiagnostics: + @pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/DiagnosticsSample.hx", + (), + min_count=1, + ) diff --git a/test/solidlsp/hlsl/test_hlsl_diagnostics.py b/test/solidlsp/hlsl/test_hlsl_diagnostics.py new file mode 100644 index 00000000..e356d2da --- /dev/null +++ b/test/solidlsp/hlsl/test_hlsl_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.hlsl +class TestHlslDiagnostics: + @pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.hlsl", + (), + min_count=1, + ) diff --git a/test/solidlsp/java/test_java_diagnostics.py b/test/solidlsp/java/test_java_diagnostics.py new file mode 100644 index 00000000..ebd59457 --- /dev/null +++ b/test/solidlsp/java/test_java_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.java +class TestJavaDiagnostics: + @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/main/java/test_repo/DiagnosticsSample.java", + (), + min_count=1, + ) diff --git a/test/solidlsp/json_ls/test_json_diagnostics.py b/test/solidlsp/json_ls/test_json_diagnostics.py new file mode 100644 index 00000000..7850083a --- /dev/null +++ b/test/solidlsp/json_ls/test_json_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.json +class TestJsonDiagnostics: + @pytest.mark.parametrize("language_server", [Language.JSON], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.json", + (), + min_count=1, + ) diff --git a/test/solidlsp/julia/test_julia_basic.py b/test/solidlsp/julia/test_julia_basic.py index 44adb43d..176587f7 100644 --- a/test/solidlsp/julia/test_julia_basic.py +++ b/test/solidlsp/julia/test_julia_basic.py @@ -6,6 +6,7 @@ from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import is_ci from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics @pytest.mark.skipif(shutil.which("julia") is None and not is_ci, reason="Julia is not available") @@ -66,3 +67,12 @@ class TestJuliaLanguageServer: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.JULIA], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.jl", + (), + min_count=1, + ) diff --git a/test/solidlsp/kotlin/test_kotlin_diagnostics.py b/test/solidlsp/kotlin/test_kotlin_diagnostics.py new file mode 100644 index 00000000..c189825f --- /dev/null +++ b/test/solidlsp/kotlin/test_kotlin_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.kotlin +class TestKotlinDiagnostics: + @pytest.mark.parametrize("language_server", [Language.KOTLIN], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/main/kotlin/test_repo/DiagnosticsSample.kt", + (), + min_count=1, + ) diff --git a/test/solidlsp/lean4/test_lean4_diagnostics.py b/test/solidlsp/lean4/test_lean4_diagnostics.py new file mode 100644 index 00000000..964bc2c6 --- /dev/null +++ b/test/solidlsp/lean4/test_lean4_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.lean4 +class TestLean4Diagnostics: + @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "DiagnosticsSample.lean", + (), + min_count=1, + ) diff --git a/test/solidlsp/matlab/test_matlab_basic.py b/test/solidlsp/matlab/test_matlab_basic.py index 71724ff7..a1582887 100644 --- a/test/solidlsp/matlab/test_matlab_basic.py +++ b/test/solidlsp/matlab/test_matlab_basic.py @@ -17,6 +17,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics # Skip all tests if MATLAB is not available pytestmark = pytest.mark.matlab @@ -129,3 +130,12 @@ class TestMatlabLanguageServerReferences: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.MATLAB], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.m", + (), + min_count=1, + ) diff --git a/test/solidlsp/nix/test_nix_basic.py b/test/solidlsp/nix/test_nix_basic.py index 23f4311a..e66ddfa5 100644 --- a/test/solidlsp/nix/test_nix_basic.py +++ b/test/solidlsp/nix/test_nix_basic.py @@ -13,6 +13,7 @@ from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from test.conftest import is_ci from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics # Skip all Nix tests on Windows as Nix doesn't support Windows pytestmark = pytest.mark.skipif(platform.system() == "Windows", reason="Nix and nil are not available on Windows") @@ -250,3 +251,12 @@ class TestNixLanguageServer: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.nix", + (), + min_count=1, + ) diff --git a/test/solidlsp/ocaml/test_ocaml_diagnostics.py b/test/solidlsp/ocaml/test_ocaml_diagnostics.py new file mode 100644 index 00000000..a76f95cb --- /dev/null +++ b/test/solidlsp/ocaml/test_ocaml_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.ocaml +class TestOcamlDiagnostics: + @pytest.mark.parametrize("language_server", [Language.OCAML], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "lib/diagnostics_sample.ml", + (), + min_count=1, + ) diff --git a/test/solidlsp/pascal/test_pascal_diagnostics.py b/test/solidlsp/pascal/test_pascal_diagnostics.py new file mode 100644 index 00000000..ad96a1ea --- /dev/null +++ b/test/solidlsp/pascal/test_pascal_diagnostics.py @@ -0,0 +1,25 @@ +import shutil + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.conftest import language_tests_enabled +from test.solidlsp.util.diagnostics import assert_file_diagnostics + +pytestmark = [ + pytest.mark.pascal, + pytest.mark.skipif(not language_tests_enabled(Language.PASCAL), reason="Pascal tests are disabled"), + pytest.mark.skipif(shutil.which("fpc") is None, reason="Pascal diagnostics require the Free Pascal compiler"), +] + + +class TestPascalDiagnostics: + @pytest.mark.parametrize("language_server", [Language.PASCAL], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.pas", + (), + min_count=1, + ) diff --git a/test/solidlsp/perl/test_perl_basic.py b/test/solidlsp/perl/test_perl_basic.py index 41682e3f..efb01d01 100644 --- a/test/solidlsp/perl/test_perl_basic.py +++ b/test/solidlsp/perl/test_perl_basic.py @@ -6,6 +6,7 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics @pytest.mark.perl @@ -88,3 +89,12 @@ class TestPerlLanguageServer: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.PERL], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.pl", + (), + min_count=1, + ) diff --git a/test/solidlsp/php/test_php_diagnostics.py b/test/solidlsp/php/test_php_diagnostics.py new file mode 100644 index 00000000..1cfc67de --- /dev/null +++ b/test/solidlsp/php/test_php_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.php +class TestPhpDiagnostics: + @pytest.mark.parametrize("language_server", [Language.PHP], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.php", + (), + min_count=1, + ) diff --git a/test/solidlsp/powershell/test_powershell_diagnostics.py b/test/solidlsp/powershell/test_powershell_diagnostics.py new file mode 100644 index 00000000..ac8706cf --- /dev/null +++ b/test/solidlsp/powershell/test_powershell_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.powershell +class TestPowershellDiagnostics: + @pytest.mark.parametrize("language_server", [Language.POWERSHELL], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.ps1", + (), + min_count=1, + ) diff --git a/test/solidlsp/python/test_python_diagnostics.py b/test/solidlsp/python/test_python_diagnostics.py new file mode 100644 index 00000000..386d2830 --- /dev/null +++ b/test/solidlsp/python/test_python_diagnostics.py @@ -0,0 +1,19 @@ +import os + +import pytest + +from solidlsp import SolidLanguageServer +from test.conftest import PYTHON_LANGUAGE_BACKENDS +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.python +class TestPythonDiagnostics: + @pytest.mark.parametrize("language_server", PYTHON_LANGUAGE_BACKENDS, indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + os.path.join("test_repo", "diagnostics_sample.py"), + ("missing_user", "undefined_name"), + min_count=2, + ) diff --git a/test/solidlsp/rust/test_rust_basic.py b/test/solidlsp/rust/test_rust_basic.py index 29fce938..4c7ec2c0 100644 --- a/test/solidlsp/rust/test_rust_basic.py +++ b/test/solidlsp/rust/test_rust_basic.py @@ -90,7 +90,7 @@ class TestRustLanguageServer: all_symbols = request_all_symbols(language_server) malformed_symbols = [] for s in all_symbols: - if has_malformed_name(s): + if has_malformed_name(s, whitespace_allowed=True): malformed_symbols.append(s) if malformed_symbols: pytest.fail( diff --git a/test/solidlsp/rust/test_rust_diagnostics.py b/test/solidlsp/rust/test_rust_diagnostics.py new file mode 100644 index 00000000..b08f7cf7 --- /dev/null +++ b/test/solidlsp/rust/test_rust_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.rust +class TestRustDiagnostics: + @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/diagnostics_sample.rs", + (), + min_count=1, + ) diff --git a/test/solidlsp/scala/test_scala_language_server.py b/test/solidlsp/scala/test_scala_language_server.py index bfc6d8f1..4e60209d 100644 --- a/test/solidlsp/scala/test_scala_language_server.py +++ b/test/solidlsp/scala/test_scala_language_server.py @@ -6,6 +6,7 @@ import pytest from solidlsp.language_servers.scala_language_server import ScalaLanguageServer from solidlsp.ls_config import Language, LanguageServerConfig from solidlsp.settings import SolidLSPSettings +from test.solidlsp.util.diagnostics import assert_file_diagnostics pytest.skip("Scala must be compiled for these tests to run through, which is a huge hassle", allow_module_level=True) @@ -62,3 +63,12 @@ def test_scala_find_definition_and_references_across_files(scala_ls): assert first_def["range"]["start"]["character"] == 6 assert first_def["range"]["end"]["line"] == 7 assert first_def["range"]["end"]["character"] == 14 + + +def test_file_diagnostics(scala_ls) -> None: + assert_file_diagnostics( + scala_ls, + "src/main/scala/com/example/DiagnosticsSample.scala", + (), + min_count=1, + ) diff --git a/test/solidlsp/solidity/test_solidity_diagnostics.py b/test/solidlsp/solidity/test_solidity_diagnostics.py new file mode 100644 index 00000000..fd3cd6ba --- /dev/null +++ b/test/solidlsp/solidity/test_solidity_diagnostics.py @@ -0,0 +1,24 @@ +import sys + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.solidity +class TestSolidityDiagnostics: + @pytest.mark.xfail( + sys.platform == "darwin", + reason="Unknown — passes on Ubuntu but consistently fails on macOS CI; root cause not yet identified.", + strict=False, + ) + @pytest.mark.parametrize("language_server", [Language.SOLIDITY], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "contracts/DiagnosticsSample.sol", + (), + min_count=1, + ) diff --git a/test/solidlsp/swift/test_swift_basic.py b/test/solidlsp/swift/test_swift_basic.py index 5a0d8bd0..30e9759c 100644 --- a/test/solidlsp/swift/test_swift_basic.py +++ b/test/solidlsp/swift/test_swift_basic.py @@ -16,6 +16,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from test.conftest import is_ci from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics # Skip Swift tests on Windows due to complex GitHub Actions configuration WINDOWS_SKIP = platform.system() == "Windows" @@ -238,3 +239,12 @@ class TestSwiftProjectBasics: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.SWIFT], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/diagnostics_sample.swift", + (), + min_count=1, + ) diff --git a/test/solidlsp/systemverilog/test_systemverilog_diagnostics.py b/test/solidlsp/systemverilog/test_systemverilog_diagnostics.py new file mode 100644 index 00000000..a783b828 --- /dev/null +++ b/test/solidlsp/systemverilog/test_systemverilog_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.systemverilog +class TestSystemverilogDiagnostics: + @pytest.mark.parametrize("language_server", [Language.SYSTEMVERILOG], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.sv", + (), + min_count=1, + ) diff --git a/test/solidlsp/terraform/test_terraform_diagnostics.py b/test/solidlsp/terraform/test_terraform_diagnostics.py new file mode 100644 index 00000000..318cc7f9 --- /dev/null +++ b/test/solidlsp/terraform/test_terraform_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.terraform +class TestTerraformDiagnostics: + @pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.tf", + (), + min_count=1, + ) diff --git a/test/solidlsp/toml/test_toml_diagnostics.py b/test/solidlsp/toml/test_toml_diagnostics.py new file mode 100644 index 00000000..dde2fc83 --- /dev/null +++ b/test/solidlsp/toml/test_toml_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.toml +class TestTomlDiagnostics: + @pytest.mark.parametrize("language_server", [Language.TOML], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.toml", + (), + min_count=1, + ) diff --git a/test/solidlsp/typescript/test_typescript_diagnostics.py b/test/solidlsp/typescript/test_typescript_diagnostics.py new file mode 100644 index 00000000..5e879e16 --- /dev/null +++ b/test/solidlsp/typescript/test_typescript_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.typescript +class TestTypeScriptDiagnostics: + @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.ts", + ("missingGreeting", "missingConsumerValue"), + min_count=2, + ) diff --git a/test/solidlsp/util/diagnostics.py b/test/solidlsp/util/diagnostics.py new file mode 100644 index 00000000..dee66086 --- /dev/null +++ b/test/solidlsp/util/diagnostics.py @@ -0,0 +1,19 @@ +from collections.abc import Sequence + +from solidlsp import SolidLanguageServer + + +def assert_file_diagnostics( + language_server: SolidLanguageServer, + relative_file_path: str, + expected_message_fragments: Sequence[str], + min_count: int = 1, +) -> None: + diagnostics = language_server.request_text_document_diagnostics(relative_file_path, min_severity=1) + + assert isinstance(diagnostics, list), diagnostics + assert len(diagnostics) >= min_count, diagnostics + + diagnostic_messages = [diagnostic["message"] for diagnostic in diagnostics] + for fragment in expected_message_fragments: + assert any(fragment in message for message in diagnostic_messages), diagnostic_messages diff --git a/test/solidlsp/vue/test_vue_diagnostics.py b/test/solidlsp/vue/test_vue_diagnostics.py new file mode 100644 index 00000000..08ac2fed --- /dev/null +++ b/test/solidlsp/vue/test_vue_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.vue +class TestVueDiagnostics: + @pytest.mark.parametrize("language_server", [Language.VUE], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/DiagnosticsSample.vue", + (), + min_count=1, + ) diff --git a/test/solidlsp/yaml_ls/test_yaml_diagnostics.py b/test/solidlsp/yaml_ls/test_yaml_diagnostics.py new file mode 100644 index 00000000..4ba774c7 --- /dev/null +++ b/test/solidlsp/yaml_ls/test_yaml_diagnostics.py @@ -0,0 +1,17 @@ +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.util.diagnostics import assert_file_diagnostics + + +@pytest.mark.yaml +class TestYamlDiagnostics: + @pytest.mark.parametrize("language_server", [Language.YAML], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "diagnostics_sample.yaml", + (), + min_count=1, + ) diff --git a/test/solidlsp/zig/test_zig_basic.py b/test/solidlsp/zig/test_zig_basic.py index 51b23650..e7413482 100644 --- a/test/solidlsp/zig/test_zig_basic.py +++ b/test/solidlsp/zig/test_zig_basic.py @@ -14,6 +14,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols +from test.solidlsp.util.diagnostics import assert_file_diagnostics @pytest.mark.zig @@ -355,3 +356,12 @@ class TestZigLanguageServer: f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + @pytest.mark.parametrize("language_server", [Language.ZIG], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/diagnostics_sample.zig", + (), + min_count=1, + )