From 249f6b07f9ccac259b0ff95e06c9a40629748e17 Mon Sep 17 00:00:00 2001 From: Vitalijs Kuprins <51091179+vkuprin@users.noreply.github.com> Date: Sat, 9 May 2026 17:49:12 +0200 Subject: [PATCH] Add Ada / SPARK language support via AdaCore Ada Language Server (#1461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces Language.ADA backed by AdaCore's Ada Language Server (ALS), the standard LSP-compliant server for Ada — and SPARK, since both share the .ads/.adb file extensions and ALS distinguishes SPARK by source-level pragmas/aspects rather than by file type. ALS is auto-downloaded from AdaCore's GitHub releases (linux-x64/arm64, darwin-x64/arm64, win32-x64) using the LanguageServerDependencyProviderSinglePath pattern, with SHA256 verification per-platform. Users can override the downloaded binary by setting ls_specific_settings.ada.ls_path to a pre-installed ada_language_server (e.g. from Alire, GNAT Studio, or the VS Code Ada extension). Test repo is a minimal multi-file GNAT project (default.gpr, helper.ads, helper.adb, main.adb). Tests modelled on test_php_basic.py: ls_running, find_definition (within/across), find_references (within/across), find_symbol, document_symbols, hierarchical document symbols, bare-symbol-name sanity check. --- CHANGELOG.md | 1 + README.md | 2 +- docs/01-about/020_programming-languages.md | 7 + pyproject.toml | 3 +- .../language_servers/ada_language_server.py | 227 ++++++++++++++++++ src/solidlsp/ls_config.py | 14 ++ test/conftest.py | 1 + test/resources/repos/ada/test_repo/.gitignore | 3 + .../resources/repos/ada/test_repo/default.gpr | 7 + .../ada/test_repo/src/diagnostics_sample.adb | 6 + .../repos/ada/test_repo/src/helper.adb | 9 + .../repos/ada/test_repo/src/helper.ads | 7 + .../repos/ada/test_repo/src/main.adb | 8 + test/solidlsp/ada/__init__.py | 0 test/solidlsp/ada/test_ada_basic.py | 144 +++++++++++ test/solidlsp/ada/test_ada_diagnostics.py | 17 ++ 16 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 src/solidlsp/language_servers/ada_language_server.py create mode 100644 test/resources/repos/ada/test_repo/.gitignore create mode 100644 test/resources/repos/ada/test_repo/default.gpr create mode 100644 test/resources/repos/ada/test_repo/src/diagnostics_sample.adb create mode 100644 test/resources/repos/ada/test_repo/src/helper.adb create mode 100644 test/resources/repos/ada/test_repo/src/helper.ads create mode 100644 test/resources/repos/ada/test_repo/src/main.adb create mode 100644 test/solidlsp/ada/__init__.py create mode 100644 test/solidlsp/ada/test_ada_basic.py create mode 100644 test/solidlsp/ada/test_ada_diagnostics.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cac85c7..6ed7dde3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Elixir (`elixir-tools/next-ls`): Fix deadlock in monorepo projects where `mix.exs` lives in a subdirectory. The server now searches immediate subdirectories when no `mix.exs` is found at the repository root. #1444 - Java (`eclipse.jdt.ls`): Add upstream JDTLS mode for offline / restricted-network use. Setting both `jdtls_path` and `lombok_path` in `ls_specific_settings.java` makes Serena use an existing upstream JDTLS installation (e.g. `brew install jdtls`) and the system JDK 21+, skipping the ~500 MB vscode-java VSIX, Gradle, and IntelliCode downloads. New related setting `java_home` lets the user override the JDK used to launch JDTLS. Default behavior unchanged — the JDTLS workspace hash is preserved bit-for-bit for users on the default route, so existing project caches are reused without a one-time reindex; the launcher path is mixed into the hash only when `jdtls_path` is set, isolating upstream installations from the default workspace. #1415 - Java (eclipse.jdt.ls): Lombok-generated methods (getters/setters, builder(), equals/hashCode/toString, etc.) are now included in symbol-based tools (find_symbol, get_symbols_overview, edits). Added lombok_show_generated setting (default: on) to toggle this. Updated bundled vscode-java to 1.54.0-923. Issue #1432. + - Add **Ada / SPARK** support using AdaCore's [Ada Language Server](https://github.com/AdaCore/ada_language_server). Auto-downloads the official prebuilt ALS binary (linux-x64/arm64, darwin-x64/arm64, win32-x64). A single `ada` language covers both Ada and SPARK, since the server uses the same `.ads`/`.adb` files for both and distinguishes SPARK by source-level pragmas/aspects. Users can override the binary by setting `ls_specific_settings.ada.ls_path` to a pre-installed `ada_language_server` (e.g. from Alire, GNAT Studio, or the VS Code Ada extension). - Add **Angular** (experimental) via a dual-server architecture: `@angular/language-server` (ngserver) handles standalone `.html` template files, while a companion `typescript-language-server` with `@angular/language-service` loaded as a tsserver plugin handles all `.ts` operations including inline templates. Provides type-aware navigation between templates and component classes. Requires Node.js, npm, and `@angular/core` installed in the project (`npm install` in the project root). Subsumes `typescript`+`html` for `.ts`/`.html` files when active; SCSS is not subsumed. - Add **HTML** (experimental) using `vscode-html-language-server` from the `vscode-langservers-extracted` npm package. Provides in-file element/id symbols via documentSymbol; cross-file references are not meaningful for HTML. Also used as a companion server by the Angular LS for plain HTML documentSymbol support. - Add **SCSS / Sass / CSS** (experimental) using [some-sass-language-server](https://github.com/wkillerud/some-sass). Handles `.scss`, `.sass`, and `.css` through one server, with full `@use`/`@forward` workspace-wide go-to-definition and find-references for variables, mixins, and functions across Sass files. The `.css` path uses the same `vscode-css-languageservice` engine that powers the standalone CSS LS; CSS feature toggles default off upstream and are flipped on at startup so symbols, hover, completion, and syntax-level diagnostics work for plain CSS as well. diff --git a/README.md b/README.md index 3bd53899..160dc8b4 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Serena incorporates a powerful abstraction layer for the integration of language The underlying language servers are typically open-source projects or at least freely available for use. When using Serena's language server backend, we provide **support for over 40 programming languages**, including -AL, Angular, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. +Ada / SPARK, AL, Angular, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, HTML, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, SCSS / Sass / CSS, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 02a37432..22fa1f66 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -28,6 +28,13 @@ or at least freely available for use. We currently provide direct, out-of-the-box support for the programming languages listed below. Some languages require additional installations or setup steps, as noted. +* **Ada / SPARK** + (uses AdaCore's [Ada Language Server (ALS)](https://github.com/AdaCore/ada_language_server), + automatically downloaded; supports `.ads`, `.adb`, and `.ada` files; + works best with a `.gpr` GNAT project file at the repository root; + SPARK is handled by the same server transparently — set language `ada` for both. + To use a pre-installed ALS (e.g. from Alire, GNAT Studio, or the VS Code Ada extension), + set `ls_specific_settings.ada.ls_path`.) * **AL** * **Angular** (experimental; requires Node.js + npm, plus `npm install` having been run in the project root so that `@angular/core` diff --git a/pyproject.toml b/pyproject.toml index 6d462f5e..278d3c3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -352,6 +352,7 @@ markers = [ "html: language server running for HTML (vscode-html-language-server, used as companion by Angular)", "scss: language server running for SCSS / Sass / CSS (some-sass-language-server)", "angular: language server running for Angular (uses @angular/language-server + @angular/language-service tsserver plugin)", + "ada: language server running for Ada / SPARK (uses AdaCore Ada Language Server)", ] [tool.codespell] @@ -359,4 +360,4 @@ markers = [ skip = '.git*,*.svg,*.lock,*.min.*' check-hidden = true ignore-regex = '\.\w+' -ignore-words-list = 'paket,EDN' +ignore-words-list = 'paket,EDN,als' diff --git a/src/solidlsp/language_servers/ada_language_server.py b/src/solidlsp/language_servers/ada_language_server.py new file mode 100644 index 00000000..bf015bd4 --- /dev/null +++ b/src/solidlsp/language_servers/ada_language_server.py @@ -0,0 +1,227 @@ +""" +Provides Ada / SPARK specific instantiation of the LanguageServer class using +AdaCore's Ada Language Server (ALS). + +ALS handles both Ada and SPARK code transparently — SPARK is identified by +pragmas/aspects in source rather than file extension, so a single +``Language.ADA`` covers both. +""" + +import logging +import os +import pathlib +import threading +from typing import cast + +from overrides import override + +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.settings import SolidLSPSettings + +from .common import RuntimeDependency, RuntimeDependencyCollection + +log = logging.getLogger(__name__) + +# Bumped on upgrades; install dir is namespaced by version so old caches stay valid. +DEFAULT_ALS_VERSION = "2026.2.202604091" +DEFAULT_ALS_SHA256_BY_PLATFORM = { + "osx-arm64": "1d25ded29b6beafcb34c9d0084d52809d84577356e467bf92e8fef32fc4216c4", + "osx-x64": "18d3277a25a6e08ce3ee7230c3e5ac20419d573cb8d8c883e7983f387a67d223", + "linux-arm64": "65c57df715df90f7581ecd6a1d1884663376baaf38cedbab80d10060ff91b03d", + "linux-x64": "2eb436a7c0e3740128cceaa15da9ff856fa75ef3fbb3e2e9d3a1bd17e15cb949", + "win-x64": "bca024dc3643b2d91aebbb747398e2e6f183ad8cb2f149e24fe7a38cdb16ed8d", +} + + +def _als_sha(version: str, platform_key: str) -> str | None: + if version == DEFAULT_ALS_VERSION: + return DEFAULT_ALS_SHA256_BY_PLATFORM[platform_key] + return None + + +# AdaCore's tarballs are downloaded directly from github.com release URLs; +# GitHub redirects asset downloads through release-assets/objects subdomains. +ALS_ALLOWED_HOSTS = ( + "github.com", + "release-assets.githubusercontent.com", + "objects.githubusercontent.com", +) + +# Each AdaCore tarball nests the binary under integration/vscode/ada/{arch}/{os}/... +# alongside any bundled shared libraries (e.g. libgmp on macOS). +_BIN_RELPATH_BY_PLATFORM = { + "osx-arm64": "integration/vscode/ada/arm64/darwin/ada_language_server", + "osx-x64": "integration/vscode/ada/x64/darwin/ada_language_server", + "linux-arm64": "integration/vscode/ada/arm64/linux/ada_language_server", + "linux-x64": "integration/vscode/ada/x64/linux/ada_language_server", + "win-x64": "integration/vscode/ada/x64/win32/ada_language_server.exe", +} + +# AdaCore uses "darwin"/"win32" in asset names; remap from Serena's platform ids. +_ASSET_SUFFIX_BY_PLATFORM = { + "osx-arm64": "darwin-arm64", + "osx-x64": "darwin-x64", + "linux-arm64": "linux-arm64", + "linux-x64": "linux-x64", + "win-x64": "win32-x64", +} + + +class AdaLanguageServer(SolidLanguageServer): + """ + Provides Ada / SPARK specific instantiation of the LanguageServer class + using AdaCore's Ada Language Server. + + You can pass the following entries in ``ls_specific_settings["ada"]``: + - ls_path: Absolute path to a pre-installed ``ada_language_server`` + executable. If set, Serena does not download ALS and uses this + binary directly. Useful for users who already have ALS available + via Alire (``alr install ada_language_server``), GNAT Studio, or + the Ada VS Code extension. + - als_version: Override the pinned ALS version downloaded by Serena + (default: the bundled Serena version). Setting this requires + ``ls_path`` since custom-version SHA256 sums are unknown. + """ + + ALS_ALLOWED_HOSTS = ALS_ALLOWED_HOSTS + + # GNAT default output directories — pruned during workspace traversal. + _IGNORED_DIRS = frozenset({"obj", "lib", ".obj", ".objects"}) + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in self._IGNORED_DIRS + + @classmethod + def _runtime_dependencies(cls, version: str) -> RuntimeDependencyCollection: + base_url = f"https://github.com/AdaCore/ada_language_server/releases/download/{version}" + return RuntimeDependencyCollection( + [ + RuntimeDependency( + id="ada-language-server", + description=f"Ada Language Server for {platform_key}", + url=f"{base_url}/als-{version}-{_ASSET_SUFFIX_BY_PLATFORM[platform_key]}.tar.gz", + platform_id=platform_key, + archive_type="gztar", + binary_name=_BIN_RELPATH_BY_PLATFORM[platform_key], + sha256=_als_sha(version, platform_key), + allowed_hosts=ALS_ALLOWED_HOSTS, + ) + for platform_key in _BIN_RELPATH_BY_PLATFORM + ] + ) + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """Setup runtime dependencies for ALS and return the path to the executable.""" + als_version = self._custom_settings.get("als_version", DEFAULT_ALS_VERSION) + deps = AdaLanguageServer._runtime_dependencies(als_version) + dependency = deps.get_single_dep_for_current_platform() + + install_dir = os.path.join(self._ls_resources_dir, f"als-{als_version}") + als_executable_path = deps.binary_path(install_dir) + if not os.path.exists(als_executable_path): + log.info( + "Downloading and extracting ada_language_server from %s to %s", + dependency.url, + install_dir, + ) + deps.install(install_dir) + if not os.path.exists(als_executable_path): + raise FileNotFoundError(f"Download failed? Could not find ada_language_server executable at {als_executable_path}") + os.chmod(als_executable_path, 0o755) + return als_executable_path + + def _create_launch_command(self, core_path: str) -> list[str]: + # ALS speaks LSP over stdio by default; no flags needed. + return [core_path] + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + """ + Creates an AdaLanguageServer instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + super().__init__( + config, + repository_root_path, + None, + "ada", + solidlsp_settings, + ) + self.server_ready = threading.Event() + + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + def _get_initialize_params(self) -> InitializeParams: + """Returns the initialization params for the Ada Language Server.""" + repository_absolute_path = self.repository_root_path + root_uri = pathlib.Path(repository_absolute_path).as_uri() + + result = { + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": {"documentChanges": True}, + "symbol": {"symbolKind": {"valueSet": list(range(1, 27))}}, + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + }, + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "publishDiagnostics": {"relatedInformation": True}, + }, + "general": {"positionEncodings": ["utf-16"]}, + }, + "initializationOptions": {}, + "trace": "off", + "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}], + } + return cast(InitializeParams, result) + + def _start_server(self) -> None: + """Start the Ada Language Server process and complete the LSP handshake.""" + + def register_capability_handler(params: dict) -> None: + return + + def window_log_message(msg: dict) -> None: + log.info("LSP: window/logMessage: %s", msg) + + def do_nothing(params: dict) -> None: + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("window/showMessage", do_nothing) + + log.info("Starting ada_language_server process") + self.server.start() + + initialize_params = self._get_initialize_params() + log.info("Sending initialize request from LSP client to LSP server and awaiting response") + init_response = self.server.send.initialize(initialize_params) + + capabilities = init_response["capabilities"] + assert "textDocumentSync" in capabilities, "ALS must support textDocumentSync" + assert "definitionProvider" in capabilities, "ALS must support textDocument/definition" + assert "documentSymbolProvider" in capabilities, "ALS must support textDocument/documentSymbol" + assert "referencesProvider" in capabilities, "ALS must support textDocument/references" + + self.server.notify.initialized({}) + self.server_ready.set() diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index cc062b29..34fdf0fb 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -110,6 +110,14 @@ class Language(str, Enum): Uses a custom LSP server based on pygls. Automatically sets up a virtual environment with pygls dependencies on first use. """ + ADA = "ada" + """Ada / SPARK language server using AdaCore's Ada Language Server (ALS). + Supports .ads (specs), .adb (bodies), and .ada files. Auto-downloads the + ALS binary from AdaCore's GitHub releases. Works best with a .gpr GNAT + project file at the repository root. SPARK files are handled transparently + by the same server, since SPARK is distinguished by pragmas/aspects in + source rather than by file extension. + """ # Experimental or deprecated Language Servers TYPESCRIPT_VTS = "typescript_vts" """Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls""" @@ -455,6 +463,8 @@ class Language(str, Enum): return FilenameMatcher(".yaml", ".yml") case self.MSL: return FilenameMatcher(".mrc") + case self.ADA: + return FilenameMatcher(".ads", ".adb", ".ada", case_sensitive=False) case self.HTML: return FilenameMatcher(".html", ".htm") case self.SCSS: @@ -702,6 +712,10 @@ class Language(str, Enum): from solidlsp.language_servers.msl_language_server import MslLanguageServer return MslLanguageServer + case self.ADA: + from solidlsp.language_servers.ada_language_server import AdaLanguageServer + + return AdaLanguageServer case self.HTML: from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer diff --git a/test/conftest.py b/test/conftest.py index e05fe340..ab93ff5f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -255,6 +255,7 @@ is_windows = platform.system() == "Windows" _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = { + Language.ADA: [pytest.mark.ada], Language.CLOJURE: [ pytest.mark.clojure, pytest.mark.skipif(not is_clojure_cli_available(), reason="clojure CLI is not installed"), diff --git a/test/resources/repos/ada/test_repo/.gitignore b/test/resources/repos/ada/test_repo/.gitignore new file mode 100644 index 00000000..5aff2b50 --- /dev/null +++ b/test/resources/repos/ada/test_repo/.gitignore @@ -0,0 +1,3 @@ +obj/ +*.ali +*.o diff --git a/test/resources/repos/ada/test_repo/default.gpr b/test/resources/repos/ada/test_repo/default.gpr new file mode 100644 index 00000000..07dbd6e9 --- /dev/null +++ b/test/resources/repos/ada/test_repo/default.gpr @@ -0,0 +1,7 @@ +project Default is + + for Source_Dirs use ("src"); + for Object_Dir use "obj"; + for Main use ("main.adb"); + +end Default; diff --git a/test/resources/repos/ada/test_repo/src/diagnostics_sample.adb b/test/resources/repos/ada/test_repo/src/diagnostics_sample.adb new file mode 100644 index 00000000..eb6c70b9 --- /dev/null +++ b/test/resources/repos/ada/test_repo/src/diagnostics_sample.adb @@ -0,0 +1,6 @@ +-- Intentionally invalid Ada — exercises the LSP diagnostics path. +-- The procedure body is missing the trailing `end Diagnostics_Sample;` token, +-- which libadalang flags as a syntax error regardless of project setup. +procedure Diagnostics_Sample is +begin + null diff --git a/test/resources/repos/ada/test_repo/src/helper.adb b/test/resources/repos/ada/test_repo/src/helper.adb new file mode 100644 index 00000000..52e818f0 --- /dev/null +++ b/test/resources/repos/ada/test_repo/src/helper.adb @@ -0,0 +1,9 @@ +package body Helper is + + function Greet (Name : String) return String is + Prefix : constant String := "Hello, "; + begin + return Prefix & Name & "!"; + end Greet; + +end Helper; diff --git a/test/resources/repos/ada/test_repo/src/helper.ads b/test/resources/repos/ada/test_repo/src/helper.ads new file mode 100644 index 00000000..63b9bf66 --- /dev/null +++ b/test/resources/repos/ada/test_repo/src/helper.ads @@ -0,0 +1,7 @@ +package Helper is + + type Greeting_Style is (Friendly, Formal); + + function Greet (Name : String) return String; + +end Helper; diff --git a/test/resources/repos/ada/test_repo/src/main.adb b/test/resources/repos/ada/test_repo/src/main.adb new file mode 100644 index 00000000..2cd34874 --- /dev/null +++ b/test/resources/repos/ada/test_repo/src/main.adb @@ -0,0 +1,8 @@ +with Ada.Text_IO; +with Helper; + +procedure Main is + Greeting : constant String := Helper.Greet ("Ada"); +begin + Ada.Text_IO.Put_Line (Greeting); +end Main; diff --git a/test/solidlsp/ada/__init__.py b/test/solidlsp/ada/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/ada/test_ada_basic.py b/test/solidlsp/ada/test_ada_basic.py new file mode 100644 index 00000000..18919a49 --- /dev/null +++ b/test/solidlsp/ada/test_ada_basic.py @@ -0,0 +1,144 @@ +from pathlib import Path + +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 + + +@pytest.mark.ada +class TestAdaLanguageServer: + """Tests for AdaCore's Ada Language Server (covers Ada and SPARK). + + Source layout (test/resources/repos/ada/test_repo): + + default.gpr + src/ + helper.ads package spec, declares ``Greet`` and ``Greeting_Style`` + helper.adb package body, defines ``Greet`` + main.adb ``with Helper;`` and calls ``Helper.Greet`` + + LSP positions are 0-indexed. Below, the layout is annotated with the + columns used by each test so that future edits to the fixtures are + obviously test-affecting. + """ + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True) + def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + assert language_server.is_running() + assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve() + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True) + def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + # main.adb (1-indexed source / 0-indexed LSP): + # 5/4: Greeting : constant String := Helper.Greet ("Ada"); + # 7/6: Ada.Text_IO.Put_Line (Greeting); + # `Greeting` in `Put_Line (Greeting)` starts at column 25 on LSP line 6. + main_path = str(repo_path / "src" / "main.adb") + definitions = language_server.request_definition(main_path, 6, 26) + + assert definitions, f"Expected non-empty definition list but got {definitions=}" + assert len(definitions) == 1 + loc = definitions[0] + assert loc["uri"].endswith("main.adb") + # `Greeting` is declared on LSP line 4; ALS points at the identifier (column 3). + assert loc["range"]["start"]["line"] == 4 + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True) + def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + # main.adb LSP line 4 / column 40 sits on the `G` of `Helper.Greet`: + # ` Greeting : constant String := Helper.Greet ("Ada");` + # ^ col 40 + # ALS resolves cross-file `Helper.Greet` to its spec declaration in helper.ads + # (LSP line 4, column 12 — the `G` of `Greet` after ` function `). + main_path = str(repo_path / "src" / "main.adb") + definitions = language_server.request_definition(main_path, 4, 40) + + assert definitions, f"Expected non-empty definition list but got {definitions=}" + assert len(definitions) == 1 + loc = definitions[0] + assert loc["uri"].endswith("helper.ads") + assert loc["range"]["start"]["line"] == 4 + assert loc["range"]["start"]["character"] == 12 + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True) + def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + # Click on `Helper` in `with Helper;` (main.adb LSP line 1, column 5). + # Serena's request_references uses includeDeclaration=False, so we need a symbol with + # at least one non-declaration usage in the same file. `Helper` is referenced as a + # context clause on line 1 and again as the qualifier of `Helper.Greet` on line 4. + main_path = str(repo_path / "src" / "main.adb") + references = language_server.request_references(main_path, 1, 5) + + assert references, f"Expected non-empty references for Helper but got {references=}" + ref_lines_in_main = {loc["range"]["start"]["line"] for loc in references if loc["uri"].endswith("main.adb")} + # The qualifier usage on LSP line 4 of main.adb must appear among the references. + assert 4 in ref_lines_in_main, f"Expected reference on line 4 of main.adb, got {ref_lines_in_main}" + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True) + def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + # Click on the `G` of `Greet` in the spec: + # helper.ads LSP line 4: ` function Greet (Name : String) return String;` + # `Greet` starts at column 12 (` ` + `function ` = 3 + 9 = 12). + spec_path = str(repo_path / "src" / "helper.ads") + references = language_server.request_references(spec_path, 4, 12) + + assert references, f"Expected non-empty references for Helper.Greet but got {references=}" + ref_files = {loc["uri"].split("/")[-1] for loc in references} + # The call site in main.adb must appear in the references. + assert "main.adb" in ref_files, f"Expected reference in main.adb, got {ref_files}" + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: + from solidlsp.ls_utils import SymbolUtils + + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "Helper"), "Helper package not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Greet"), "Greet subprogram not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main procedure not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + def test_document_symbols_helper(self, language_server: SolidLanguageServer) -> None: + doc_symbols = language_server.request_document_symbols(str(Path("src") / "helper.ads")) + all_symbols, _ = doc_symbols.get_all_symbols_and_roots() + names = {sym.get("name") for sym in all_symbols if sym.get("name")} + assert "Helper" in names, f"Helper package not found in helper.ads document symbols. Found: {names}" + assert "Greet" in names, f"Greet not found in helper.ads document symbols. Found: {names}" + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + def test_document_symbols_hierarchical_structure(self, language_server: SolidLanguageServer) -> None: + """ALS must return hierarchical DocumentSymbol[] with subprograms nested under their package.""" + all_symbols, root_symbols = language_server.request_document_symbols(str(Path("src") / "helper.ads")).get_all_symbols_and_roots() + + root_names = [s.get("name") for s in root_symbols] + assert "Helper" in root_names, f"Helper package not at root level. Roots: {root_names}" + + helper_symbol = next((s for s in root_symbols if s.get("name") == "Helper"), None) + assert helper_symbol is not None, "Helper package missing from root symbols" + helper_children = helper_symbol.get("children", []) + helper_child_names = [c.get("name") for c in helper_children] + assert helper_child_names, f"Helper package has no children — hierarchicalDocumentSymbolSupport is not working. Roots: {root_names}" + assert "Greet" in helper_child_names, f"Greet not nested under Helper. Children: {helper_child_names}" + + # Greet must NOT appear at root level — that would indicate the flat fallback format. + assert "Greet" not in root_names, f"Greet should be a child of Helper, not at root level. Roots: {root_names}" + + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None: + # ALS surfaces a few Ada-specific synthetic groupings as symbols: + # - "With clauses" — namespace-kind group containing all `with` statements in a unit + # - dotted unit names like "Ada.Text_IO" — references to library packages + # Both are legitimate names for Ada; allow whitespace and periods accordingly. + all_symbols = request_all_symbols(language_server) + malformed_symbols = [s for s in all_symbols if has_malformed_name(s, whitespace_allowed=True, period_allowed=True)] + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + ) diff --git a/test/solidlsp/ada/test_ada_diagnostics.py b/test/solidlsp/ada/test_ada_diagnostics.py new file mode 100644 index 00000000..cac0baa1 --- /dev/null +++ b/test/solidlsp/ada/test_ada_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.ada +class TestAdaDiagnostics: + @pytest.mark.parametrize("language_server", [Language.ADA], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + assert_file_diagnostics( + language_server, + "src/diagnostics_sample.adb", + (), + min_count=1, + )