diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index aed77355..58ba0032 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -138,7 +138,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - name: Setup .NET SDK (for F# and C# languages) uses: actions/setup-dotnet@v4 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ed7dde3..bffc202c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Status of the `main` branch. Changes prior to the next official version change w - 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. + - Add **1C / OneScript** support using [BSL Language Server](https://github.com/1c-syntax/bsl-language-server/). - Add support for more filenames to be considered by ccls and clangd. - Clojure (`clojure-lsp`): Fix incomplete `find_referencing_symbols` results in multi-module monorepos. clojure-lsp only discovers source paths from the descriptor at the workspace root and does not recurse for sub-module `deps.edn` / `project.clj` / `shadow-cljs.edn` / `bb.edn` files, so references in sibling modules were silently missed until those files happened to be opened by `find_symbol` / `get_symbols_overview`. Serena now scans the repo for project descriptors at startup and passes the union of their declared source paths to clojure-lsp via `initializationOptions`. Project-local `.lsp/config.edn` files are honoured as-is (no override). New `ls_specific_settings.clojure` keys: `source_paths` (explicit override) and `config_edn_path` (parse `:source-paths` from a user-supplied config file). diff --git a/README.md b/README.md index 160dc8b4..9fe71225 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 -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. +Ada / SPARK, AL, Angular, Ansible, Bash, BSL, 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 22fa1f66..95601e85 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -47,6 +47,8 @@ Some languages require additional installations or setup steps, as noted. semantic tokens, and validation; document symbols, workspace symbols, references, and rename are not supported by this version) * **Bash** +* **BSL** (1C:Enterprise / OneScript) + (requires Java 21+ on PATH; uses [bsl-language-server](https://github.com/1c-syntax/bsl-language-server) by 1c-syntax; the JAR is auto-downloaded and SHA-256-verified for the bundled default version; supports `.bsl` and `.os` files; configure optional `ls_path` or `bsl_ls_version` under `ls_specific_settings.bsl`) * **C#** (by default, uses the Roslyn language server (language `csharp`), requiring [.NET v10+](https://dotnet.microsoft.com/en-us/download/dotnet) and, on Windows, `pwsh` ([PowerShell 7+](https://learn.microsoft.com/en-us/powershell/scripting/install/install-powershell-on-windows?view=powershell-7.5)); set language to `csharp_omnisharp` to use OmiSharp instead) diff --git a/docs/02-usage/050_configuration.md b/docs/02-usage/050_configuration.md index e600d1a4..ddc095b1 100644 --- a/docs/02-usage/050_configuration.md +++ b/docs/02-usage/050_configuration.md @@ -227,7 +227,7 @@ ls_specific_settings: This is supported by all language servers deriving their dependency provider from `LanguageServerDependencyProviderSinglePath`, and by some additional wrappers that explicitly expose `ls_path`. -Common examples include: `ansible`, `bash`, `clojure`, `cpp`, `cpp_ccls`, `hlsl`, `html`, `kotlin`, `lean4`, `luau`, `markdown`, `php`, +Common examples include: `ansible`, `bash`, `bsl`, `clojure`, `cpp`, `cpp_ccls`, `hlsl`, `html`, `kotlin`, `lean4`, `luau`, `markdown`, `php`, `php_phpactor`, `python`, `rust`, `scss`, `solidity`, `systemverilog`, `toml`, `typescript`, and `yaml`. Note: `angular` does **not** support `ls_path` — the Angular language server is part of a multi-process orchestration @@ -308,6 +308,29 @@ Supported settings: | `bash_language_server_version` | `5.6.0` | Override the npm package version Serena installs when `ls_path` is not set. | | `npm_registry` | `null` | Override the npm registry Serena uses for the managed install. | +#### BSL (1C:Enterprise / OneScript) + +Serena uses [bsl-language-server](https://github.com/1c-syntax/bsl-language-server) by 1c-syntax +for BSL support. The JAR is downloaded automatically on first use and SHA-256-verified for the +bundled default version. **Requires Java 21+ on `PATH`** — bsl-language-server v0.29.0 is built +with `targetCompatibility = JavaVersion.VERSION_21` and fails to launch under older JDKs. + +Supported settings: + +| Setting | Default | Description | +|---|---|---| +| `ls_path` | managed download | Override the path to an existing `bsl-language-server-*-exec.jar`. When set, Serena does not download anything; the JAR is launched directly via `java -jar`. | +| `bsl_ls_version` | `0.29.0` | Override the bsl-language-server release version Serena downloads when `ls_path` is not set. SHA-256 verification is performed only for the default version; user-overridden versions install without SHA verification. | + +Example: + +```yaml +ls_specific_settings: + bsl: + bsl_ls_version: "0.29.0" + # ls_path: "/opt/bsl/bsl-language-server-0.29.0-exec.jar" # optional +``` + #### Clojure Serena uses `clojure-lsp` for Clojure support. diff --git a/pyproject.toml b/pyproject.toml index 278d3c3a..56a58d95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -294,6 +294,9 @@ max-complexity = 20 [tool.ruff.lint.per-file-ignores] "tests/**" = ["D103"] "scripts/**" = ["D103"] +# BSL integration tests deliberately contain Cyrillic identifiers (1C:Enterprise metadata +# object names and procedure names). ruff's ambiguous-unicode rules flag every such char. +"test/solidlsp/bsl/**" = ["RUF001", "RUF002", "RUF003"] [tool.pytest.ini_options] addopts = "--snapshot-patch-pycharm-diff" @@ -349,6 +352,7 @@ markers = [ "solidity: language server running for Solidity (uses @nomicfoundation/solidity-language-server)", "ansible: language server running for Ansible (uses @ansible/ansible-language-server)", "msl: language server running for mSL (mIRC Scripting Language)", + "bsl: language server running for BSL (1C:Enterprise / OneScript)", "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)", diff --git a/src/solidlsp/language_servers/bsl_language_server.py b/src/solidlsp/language_servers/bsl_language_server.py new file mode 100644 index 00000000..98aaa0e1 --- /dev/null +++ b/src/solidlsp/language_servers/bsl_language_server.py @@ -0,0 +1,226 @@ +""" +Provides BSL (1C:Enterprise) specific instantiation of the LanguageServer class +using bsl-language-server by 1c-syntax. Supports .bsl and .os files. +Requires Java 21+ on PATH (bsl-language-server v0.29.0 is built with +``targetCompatibility = JavaVersion.VERSION_21``). + +You can configure the following options in ls_specific_settings (in serena_config.yml): + + ls_specific_settings: + bsl: + ls_path: '/path/to/bsl-language-server.jar' # Custom path to BSL Language Server JAR + bsl_ls_version: '0.29.0' # BSL Language Server version (default: current bundled version) +""" + +import logging +import os +import pathlib +import re +import shutil +import subprocess +import threading + +from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection +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 + +log = logging.getLogger(__name__) + +DEFAULT_BSL_LS_VERSION = "0.29.0" +BSL_LS_JAR_SHA256_BY_VERSION = { + "0.29.0": "d6fa9ad638ba51855e260b88ad1f8ce4e602385845a4ee43600d148f779bcf0b", +} +BSL_LS_MIN_JAVA_VERSION = 21 +"""Minimum Java major version required to run the bundled bsl-language-server JAR. + +bsl-language-server v0.29.0 is compiled with ``targetCompatibility = VERSION_21`` and +therefore fails at class-load time on older JDKs (``UnsupportedClassVersionError``), +which manifests as ``LanguageServerTerminatedException`` in Serena. We enforce the +check up front so the user gets an actionable error instead of a dead LSP process. +""" + + +def _get_java_major_version(java_exe: str) -> int | None: + """Returns the detected Java major version (e.g. 21) or ``None`` if detection fails. + + :param java_exe: path to the ``java`` executable. + """ + try: + result = subprocess.run( + [java_exe, "-version"], + capture_output=True, + text=True, + timeout=15, + check=False, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + log.warning("Failed to invoke '%s -version': %s", java_exe, exc) + return None + + output = (result.stderr or "") + "\n" + (result.stdout or "") + # modern JDKs print: openjdk version "21.0.2" 2024-01-16 + # legacy JDKs print: java version "1.8.0_402" (major = 8) + m = re.search(r'version "(\d+)(?:\.(\d+))?', output) + if not m: + log.warning("Could not parse Java version from output of '%s -version': %s", java_exe, output.strip()[:200]) + return None + + first = int(m.group(1)) + if first == 1 and m.group(2): + return int(m.group(2)) # 1.8 -> 8 + return first + + +def _verify_java_available() -> None: + """Ensures a suitable ``java`` (>= :data:`BSL_LS_MIN_JAVA_VERSION`) is on ``PATH``. + + :raises RuntimeError: if ``java`` is missing or its major version is too low. + """ + java_exe = shutil.which("java") + if java_exe is None: + raise RuntimeError(f"Java {BSL_LS_MIN_JAVA_VERSION}+ is required for BSL Language Server but 'java' was not found on PATH.") + + major = _get_java_major_version(java_exe) + if major is None: + # detection failed; log a warning but let the LSP attempt to start + log.warning("Could not determine Java version for '%s'; BSL LSP requires Java %d+.", java_exe, BSL_LS_MIN_JAVA_VERSION) + return + + if major < BSL_LS_MIN_JAVA_VERSION: + raise RuntimeError( + f"Java {BSL_LS_MIN_JAVA_VERSION}+ is required for BSL Language Server, but '{java_exe}' is Java {major}. " + f"Install a newer JDK (e.g. Temurin {BSL_LS_MIN_JAVA_VERSION}) and ensure it appears first on PATH " + f"or point JAVA_HOME to it." + ) + + +class BSLLanguageServer(SolidLanguageServer): + """ + BSL (1C:Enterprise / OneScript) language server integration for Serena. + """ + + def __init__( + self, + config: LanguageServerConfig, + repository_root_path: str, + solidlsp_settings: SolidLSPSettings, + ): + super().__init__( + config, + repository_root_path, + None, + "bsl", + solidlsp_settings, + ) + self.server_ready = threading.Event() + + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + # resolve target version; custom versions intentionally skip SHA verification below + bsl_version = self._custom_settings.get("bsl_ls_version", DEFAULT_BSL_LS_VERSION) + user_overrode_version = bsl_version != DEFAULT_BSL_LS_VERSION + + jar_dir = os.path.join(self._ls_resources_dir, f"bsl-ls-{bsl_version}") + jar_path = os.path.join(jar_dir, f"bsl-language-server-{bsl_version}-exec.jar") + + # download only when the versioned JAR is missing + if not os.path.exists(jar_path): + jar_url = ( + f"https://github.com/1c-syntax/bsl-language-server/releases/download/" + f"v{bsl_version}/bsl-language-server-{bsl_version}-exec.jar" + ) + expected_sha256 = None if user_overrode_version else BSL_LS_JAR_SHA256_BY_VERSION.get(bsl_version) + + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="bsl-language-server", + description="BSL Language Server JAR by 1c-syntax", + url=jar_url, + sha256=expected_sha256, + archive_type="binary", + binary_name=f"bsl-language-server-{bsl_version}-exec.jar", + platform_id="any", + ), + ] + ) + deps.install(jar_dir) + + if not os.path.exists(jar_path): + raise FileNotFoundError(f"BSL Language Server JAR not found at {jar_path} after installation.") + + log.info(f"Using BSL Language Server v{bsl_version} at {jar_path}") + return jar_path + + def _create_launch_command(self, core_path: str) -> list[str]: + # Java availability+version is required for both managed installs and user-provided jars (ls_path) + _verify_java_available() + return ["java", "-jar", core_path] + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + root_uri = pathlib.Path(repository_absolute_path).as_uri() + return { # type: ignore[return-value] + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, # type: ignore + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, # type: ignore + "rename": {"dynamicRegistration": True, "prepareSupport": True}, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + "symbol": {"dynamicRegistration": True}, + }, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + {"uri": root_uri, "name": os.path.basename(repository_absolute_path)}, + ], + } + + def _start_server(self) -> None: + def window_log_message(msg: dict) -> None: + log.info("BSL LSP: %s", msg.get("message", "")) + + def do_nothing(_: dict) -> None: + return + + 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_request( + "client/registerCapability", + lambda params: None, + ) + + log.info("Starting BSL language server process") + self.server.start() + + init_params = self._get_initialize_params(self.repository_root_path) + init_response = self.server.send.initialize(init_params) + log.debug("BSL LSP initialize response: %s", init_response) + + assert "capabilities" in init_response, "BSL LSP did not return capabilities" + + self.server.notify.initialized({}) + self.server_ready.set() diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index 34fdf0fb..0817c3a3 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -110,6 +110,11 @@ class Language(str, Enum): Uses a custom LSP server based on pygls. Automatically sets up a virtual environment with pygls dependencies on first use. """ + BSL = "bsl" + """BSL Language Server for 1C:Enterprise and OneScript languages. + Uses bsl-language-server by 1c-syntax. Automatically downloads the JAR. + Supports .bsl and .os files. Requires Java 21+ on PATH. + """ ADA = "ada" """Ada / SPARK language server using AdaCore's Ada Language Server (ALS). Supports .ads (specs), .adb (bodies), and .ada files. Auto-downloads the @@ -463,6 +468,8 @@ class Language(str, Enum): return FilenameMatcher(".yaml", ".yml") case self.MSL: return FilenameMatcher(".mrc") + case self.BSL: + return FilenameMatcher(".bsl", ".os") case self.ADA: return FilenameMatcher(".ads", ".adb", ".ada", case_sensitive=False) case self.HTML: @@ -515,7 +522,7 @@ class Language(str, Enum): return CSharpLanguageServer case self.CSHARP_OMNISHARP: - from solidlsp.language_servers.omnisharp import OmniSharp + from solidlsp.language_servers.omnisharp import OmniSharp # type: ignore[attr-defined] return OmniSharp case self.TYPESCRIPT: @@ -712,6 +719,10 @@ class Language(str, Enum): from solidlsp.language_servers.msl_language_server import MslLanguageServer return MslLanguageServer + case self.BSL: + from solidlsp.language_servers.bsl_language_server import BSLLanguageServer + + return BSLLanguageServer case self.ADA: from solidlsp.language_servers.ada_language_server import AdaLanguageServer diff --git a/test/conftest.py b/test/conftest.py index ab93ff5f..95ef22cd 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -278,6 +278,10 @@ _LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = { Language.PYTHON_TY: [pytest.mark.python], Language.RUST: [pytest.mark.rust], Language.TYPESCRIPT: [pytest.mark.typescript], + Language.BSL: [ + pytest.mark.bsl, + pytest.mark.skipif(_sh.which("java") is None, reason="Java is not installed"), + ], Language.ANGULAR: [pytest.mark.angular], Language.HTML: [pytest.mark.html], Language.SCSS: [pytest.mark.scss], @@ -331,6 +335,10 @@ def _determine_disabled_languages() -> list[Language]: if not al_tests_enabled: result.append(Language.AL) + # Disable BSL tests only when Java is not available (Java IS present in CI via actions/setup-java) + if _sh.which("java") is None: + result.append(Language.BSL) + return result diff --git a/test/resources/repos/bsl/test_repo/.bsl-language-server.json b/test/resources/repos/bsl/test_repo/.bsl-language-server.json new file mode 100644 index 00000000..45c1c84c --- /dev/null +++ b/test/resources/repos/bsl/test_repo/.bsl-language-server.json @@ -0,0 +1,5 @@ +{ + "language": "ru", + "diagnosticLanguage": "ru", + "computeDiagnostics": "onSave" +} \ No newline at end of file diff --git a/test/resources/repos/bsl/test_repo/.gitkeep b/test/resources/repos/bsl/test_repo/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/resources/repos/bsl/test_repo/CommonModule.bsl b/test/resources/repos/bsl/test_repo/CommonModule.bsl new file mode 100644 index 00000000..ce5b6b9e --- /dev/null +++ b/test/resources/repos/bsl/test_repo/CommonModule.bsl @@ -0,0 +1,14 @@ +// CommonModule.bsl - common module with reusable procedures + +Процедура ВывестиСообщение(Текст) Экспорт + Сообщить(Текст); +КонецПроцедуры + +Функция ПолучитьПриветствие(Имя) Экспорт + Возврат "Привет, " + Имя + "!"; +КонецФункции + +Процедура ВызватьПриветствие() Экспорт + Сообщение = ПолучитьПриветствие("Мир"); + ВывестиСообщение(Сообщение); +КонецПроцедуры diff --git a/test/resources/repos/bsl/test_repo/ObjectModule.bsl b/test/resources/repos/bsl/test_repo/ObjectModule.bsl new file mode 100644 index 00000000..88cc6938 --- /dev/null +++ b/test/resources/repos/bsl/test_repo/ObjectModule.bsl @@ -0,0 +1,12 @@ +// ObjectModule.bsl - object module example + +Перем ВнутреннееСостояние; + +Процедура Инициализировать() Экспорт + ВнутреннееСостояние = "ready"; + ВывестиСообщение("Объект инициализирован"); +КонецПроцедуры + +Функция ПолучитьСостояние() Экспорт + Возврат ВнутреннееСостояние; +КонецФункции diff --git a/test/resources/repos/bsl/test_repo/diagnostics_sample.bsl b/test/resources/repos/bsl/test_repo/diagnostics_sample.bsl new file mode 100644 index 00000000..3d0ba312 --- /dev/null +++ b/test/resources/repos/bsl/test_repo/diagnostics_sample.bsl @@ -0,0 +1,6 @@ +// diagnostics_sample.bsl — намеренно сломанный файл для проверки диагностик BSL LS. +// Содержит синтаксическую ошибку (незакрытая строка), которую bsl-language-server гарантированно флагнет. + +Процедура СломаннаяПроцедура() Экспорт + Текст = "незакрытая строка; +КонецПроцедуры diff --git a/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль1.xml b/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль1.xml new file mode 100644 index 00000000..cc693c58 --- /dev/null +++ b/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль1.xml @@ -0,0 +1,18 @@ + + + + + ОбщийМодуль1 + + + false + false + true + true + false + true + false + DontUse + + + \ No newline at end of file diff --git a/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль1/Ext/Module.bsl b/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль1/Ext/Module.bsl new file mode 100644 index 00000000..c17ba301 --- /dev/null +++ b/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль1/Ext/Module.bsl @@ -0,0 +1,5 @@ +// CommonModule1 — shared helpers called from other modules. + +Процедура ВывестиСообщение(Текст) Экспорт + Сообщить(Текст); +КонецПроцедуры diff --git a/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль2.xml b/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль2.xml new file mode 100644 index 00000000..6c539843 --- /dev/null +++ b/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль2.xml @@ -0,0 +1,18 @@ + + + + + ОбщийМодуль2 + + + false + false + true + true + false + true + false + DontUse + + + \ No newline at end of file diff --git a/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль2/Ext/Module.bsl b/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль2/Ext/Module.bsl new file mode 100644 index 00000000..923fb56d --- /dev/null +++ b/test/resources/repos/bsl/test_repo/src/CommonModules/ОбщийМодуль2/Ext/Module.bsl @@ -0,0 +1,5 @@ +// CommonModule2 — exercises a cross-module call into CommonModule1. + +Процедура ПозватьМодуль1() Экспорт + ОбщийМодуль1.ВывестиСообщение("Hello from CommonModule2"); +КонецПроцедуры diff --git a/test/resources/repos/bsl/test_repo/src/ConfigDumpInfo.xml b/test/resources/repos/bsl/test_repo/src/ConfigDumpInfo.xml new file mode 100644 index 00000000..899c546c --- /dev/null +++ b/test/resources/repos/bsl/test_repo/src/ConfigDumpInfo.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test/resources/repos/bsl/test_repo/src/Configuration.xml b/test/resources/repos/bsl/test_repo/src/Configuration.xml new file mode 100644 index 00000000..0ce60408 --- /dev/null +++ b/test/resources/repos/bsl/test_repo/src/Configuration.xml @@ -0,0 +1,252 @@ + + + + + + 9cd510cd-abfc-11d4-9434-004095e12fc7 + f2bbb9be-74c9-40ac-a945-4b03d2624a80 + + + 9fcd25a0-4822-11d4-9414-008048da11f9 + 74c2c4b9-ca2e-4c29-9ac1-c9a686dffd48 + + + e3687481-0a87-462c-a166-9f34594f9bba + e6d84959-ef42-417d-b585-89b178e2ebe4 + + + 9de14907-ec23-4a07-96f0-85521cb6b53b + bb344884-c3a2-4197-ad5d-7e6c391de24f + + + 51f2d5d8-ea4d-4064-8892-82951750031e + 4c2a3610-e746-47e5-a127-f6ef59fba16d + + + e68182ea-4237-4383-967f-90c1e3370bc7 + f94ef4a2-9b6b-4ef7-8fc1-b69a351798cd + + + fb282519-d103-4dd3-bc12-cb271d631dfc + d1f9e184-8cf0-47a9-a487-22516bb66c8c + + + + Конфигурация + + + + Version8_3_27 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + TextToSpeech + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + Taxi + DontUse + Version8_3_27 + + + + Русский + ОбщийМодуль1 + ОбщийМодуль2 + + + \ No newline at end of file diff --git a/test/resources/repos/bsl/test_repo/src/Languages/Русский.xml b/test/resources/repos/bsl/test_repo/src/Languages/Русский.xml new file mode 100644 index 00000000..6665f2c9 --- /dev/null +++ b/test/resources/repos/bsl/test_repo/src/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/test/solidlsp/bsl/__init__.py b/test/solidlsp/bsl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/bsl/test_bsl_basic.py b/test/solidlsp/bsl/test_bsl_basic.py new file mode 100644 index 00000000..b58ab984 --- /dev/null +++ b/test/solidlsp/bsl/test_bsl_basic.py @@ -0,0 +1,269 @@ +import os +from pathlib import Path +from unittest import mock + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +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 + + +@pytest.mark.bsl +class TestBSLLanguageServer: + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.BSL], indirect=True) + def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + """Language server starts and attaches to the test repository.""" + assert language_server.is_running() + assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve() + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_find_symbol(self, language_server: SolidLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "ВывестиСообщение"), "ВывестиСообщение not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "ПолучитьПриветствие"), "ПолучитьПриветствие not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Инициализировать"), "Инициализировать not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_document_symbols(self, language_server: SolidLanguageServer) -> None: + doc_symbols = language_server.request_document_symbols("CommonModule.bsl") + all_symbols, _ = doc_symbols.get_all_symbols_and_roots() + names = [s.get("name") for s in all_symbols if s.get("name")] + assert "ВывестиСообщение" in names, f"ВывестиСообщение not found in CommonModule.bsl symbols. Found: {names}" + assert "ПолучитьПриветствие" in names, f"ПолучитьПриветствие not found in CommonModule.bsl symbols. Found: {names}" + assert "ВызватьПриветствие" in names, f"ВызватьПриветствие not found in CommonModule.bsl symbols. Found: {names}" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_full_symbol_tree_within_file(self, language_server: SolidLanguageServer) -> None: + """Scoping the full-tree request to a single file returns that file's symbols.""" + symbols = language_server.request_full_symbol_tree(within_relative_path="ObjectModule.bsl") + assert SymbolUtils.symbol_tree_contains_name(symbols, "Инициализировать"), ( + "Инициализировать not found in ObjectModule.bsl symbol tree" + ) + assert SymbolUtils.symbol_tree_contains_name(symbols, "ПолучитьСостояние"), ( + "ПолучитьСостояние not found in ObjectModule.bsl symbol tree" + ) + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None: + # CommonModule.bsl (0-indexed): + # line 2: Процедура ВывестиСообщение(Текст) Экспорт <- declaration (name starts at col 10) + # line 13: ВывестиСообщение(Сообщение); <- internal call + # This test asserts within-file resolution on a bare ``.bsl`` file (no surrounding + # 1C project metadata). Cross-module resolution is exercised separately via the + # dedicated ``src/`` fixture in ``test_find_references_across_files`` below. + refs = language_server.request_references("CommonModule.bsl", line=2, column=10) + assert refs, "Expected at least one reference to ВывестиСообщение" + file_names = [ref.get("relativePath", "") for ref in refs] + assert any("CommonModule.bsl" in f for f in file_names), f"Expected self-reference in CommonModule.bsl, got: {file_names}" + # the internal call is on line 13 + matching_lines = [ref["range"]["start"]["line"] for ref in refs if "CommonModule.bsl" in ref.get("relativePath", "")] + assert 12 in matching_lines, f"Expected a reference on line 12 (0-indexed), got lines: {matching_lines}" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_find_references_to_function_within_file(self, language_server: SolidLanguageServer) -> None: + # CommonModule.bsl (0-indexed): + # line 6: Функция ПолучитьПриветствие(Имя) Экспорт <- declaration (name starts at col 8) + # line 12: Сообщение = ПолучитьПриветствие("Мир"); <- internal call at col 16 + refs = language_server.request_references("CommonModule.bsl", line=6, column=8) + assert refs, "Expected at least one reference to ПолучитьПриветствие" + matching_lines = [ref["range"]["start"]["line"] for ref in refs if "CommonModule.bsl" in ref.get("relativePath", "")] + assert 11 in matching_lines, f"Expected a reference on line 11 (0-indexed), got lines: {matching_lines}" + + # --- Cross-file reference resolution --- + # + # bsl-language-server links common modules across files only when the fixture ships + # proper 1C Configuration metadata (``Configuration.xml`` + per-module ``.mdo`` / + # ``Ext/Module.bsl`` layout). The ``src/`` subtree under the BSL test repo provides + # a minimal-but-complete Designer-format dump for exactly this purpose. The two + # tests below request references and the go-to-definition from the cross-module + # call site ``ОбщийМодуль1.ВывестиСообщение(...)`` in ``ОбщийМодуль2``. + + _CROSS_REF_MODULE1 = os.path.join("src", "CommonModules", "ОбщийМодуль1", "Ext", "Module.bsl") + _CROSS_REF_MODULE2 = os.path.join("src", "CommonModules", "ОбщийМодуль2", "Ext", "Module.bsl") + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None: + """``request_references`` from the declaration must include the cross-module call-site.""" + # ОбщийМодуль1 / Ext / Module.bsl (0-indexed): + # line 2: Процедура ВывестиСообщение(Текст) Экспорт <- declaration (name at col 10) + # ОбщийМодуль2 / Ext / Module.bsl (0-indexed): + # line 3: ОбщийМодуль1.ВывестиСообщение("Hello from CommonModule2"); <- cross-module call (col 17) + refs = language_server.request_references(self._CROSS_REF_MODULE1, line=2, column=10) + assert refs, "Expected at least one reference to ВывестиСообщение across modules" + call_site_paths = [ref.get("relativePath", "") for ref in refs if "ОбщийМодуль2" in ref.get("relativePath", "")] + assert call_site_paths, ( + f"Expected a cross-module reference from ОбщийМодуль2, got relativePaths: {[ref.get('relativePath', '') for ref in refs]}" + ) + call_site_lines = [ref["range"]["start"]["line"] for ref in refs if "ОбщийМодуль2" in ref.get("relativePath", "")] + assert 3 in call_site_lines, f"Expected a reference at ОбщийМодуль2/Module.bsl line 3, got lines: {call_site_lines}" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_find_definition_across_files(self, language_server: SolidLanguageServer) -> None: + """``request_definition`` from a cross-module call-site must resolve to the other module.""" + # cursor on "ВывестиСообщение" inside + # ОбщийМодуль1.ВывестиСообщение("Hello from CommonModule2"); (line 3, col 17 — 0-indexed) + definitions = language_server.request_definition(self._CROSS_REF_MODULE2, line=3, column=17) + assert definitions, "Expected a cross-module definition for ВывестиСообщение" + target_paths = [d.get("relativePath", "") for d in definitions] + assert any("ОбщийМодуль1" in p for p in target_paths), f"Expected definition to resolve to ОбщийМодуль1, got: {target_paths}" + target_lines = [d["range"]["start"]["line"] for d in definitions if "ОбщийМодуль1" in d.get("relativePath", "")] + assert 2 in target_lines, f"Expected the definition to point at ОбщийМодуль1/Module.bsl line 2, got: {target_lines}" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None: + all_symbols = request_all_symbols(language_server) + malformed = [s for s in all_symbols if has_malformed_name(s)] + if malformed: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(s) for s in malformed]}", + pytrace=False, + ) + + +# --------------------------------------------------------------------------- +# Unit tests — no language server needed, always run regardless of Java +# --------------------------------------------------------------------------- + + +def test_bsl_filename_matcher() -> None: + matcher = Language.BSL.get_source_fn_matcher() + assert matcher.is_relevant_filename("module.bsl") + assert matcher.is_relevant_filename("script.os") + assert not matcher.is_relevant_filename("module.py") + + +def test_bsl_enum_registration() -> None: + assert Language.BSL.value == "bsl" + assert Language.BSL.get_ls_class().__name__ == "BSLLanguageServer" + + +def test_bsl_dependency_provider_default_version() -> None: + """DependencyProvider resolves the default version to the expected versioned JAR path.""" + from solidlsp.language_servers.bsl_language_server import ( + DEFAULT_BSL_LS_VERSION, + BSLLanguageServer, + ) + + settings = SolidLSPSettings() + provider = BSLLanguageServer.DependencyProvider( + settings.get_ls_specific_settings(Language.BSL), + "/tmp/ls_resources", + ) + + expected_version = DEFAULT_BSL_LS_VERSION + expected_jar_dir = os.path.join("/tmp/ls_resources", f"bsl-ls-{expected_version}") + expected_jar_path = os.path.join(expected_jar_dir, f"bsl-language-server-{expected_version}-exec.jar") + + # pretend JAR is already on disk so no download is attempted + with mock.patch("os.path.exists", return_value=True): + jar_path = provider._get_or_install_core_dependency() + + assert jar_path == expected_jar_path + + +def test_bsl_dependency_provider_custom_version_no_sha() -> None: + """User-overridden versions must install without SHA256 verification.""" + from solidlsp.language_servers.bsl_language_server import BSLLanguageServer + from solidlsp.language_servers.common import RuntimeDependencyCollection + + settings = SolidLSPSettings() + settings.ls_specific_settings[Language.BSL] = {"bsl_ls_version": "0.28.0"} + provider = BSLLanguageServer.DependencyProvider( + settings.get_ls_specific_settings(Language.BSL), + "/tmp/ls_resources", + ) + + custom_version = "0.28.0" + expected_jar_dir = os.path.join("/tmp/ls_resources", f"bsl-ls-{custom_version}") + expected_jar_path = os.path.join(expected_jar_dir, f"bsl-language-server-{custom_version}-exec.jar") + + installed_deps = [] + + def fake_install(self_inner, install_dir): + installed_deps.extend(self_inner.get_dependencies_for_current_platform()) + os.makedirs(install_dir, exist_ok=True) + open(expected_jar_path, "w").close() + + with mock.patch.object(RuntimeDependencyCollection, "install", fake_install): + jar_path = provider._get_or_install_core_dependency() + + assert jar_path == expected_jar_path + assert len(installed_deps) == 1 + assert installed_deps[0].sha256 is None, "SHA256 must be None for user-overridden version" + + if os.path.exists(expected_jar_path): + os.remove(expected_jar_path) + if os.path.exists(expected_jar_dir): + os.rmdir(expected_jar_dir) + + +def test_bsl_launch_command_uses_ls_path_without_download() -> None: + """ + When ``ls_path`` is set, the public launch-command flow must return the user's JAR + unchanged and must NOT invoke the download / install path. This covers the real + code path used at runtime (``create_launch_command``), not just private helpers. + """ + from solidlsp.language_servers import bsl_language_server + from solidlsp.language_servers.bsl_language_server import BSLLanguageServer + + settings = SolidLSPSettings() + settings.ls_specific_settings[Language.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"} + provider = BSLLanguageServer.DependencyProvider( + settings.get_ls_specific_settings(Language.BSL), + "/tmp/ls_resources", + ) + + with ( + mock.patch.object(bsl_language_server.shutil, "which", return_value="/usr/bin/java"), + mock.patch.object(bsl_language_server, "_get_java_major_version", return_value=21), + mock.patch.object( + BSLLanguageServer.DependencyProvider, + "_get_or_install_core_dependency", + side_effect=AssertionError("must not be called when ls_path is provided"), + ) as install_mock, + ): + cmd = provider.create_launch_command() + + assert cmd == ["java", "-jar", "/custom/path/bsl-language-server.jar"] + install_mock.assert_not_called() + + +def test_bsl_launch_command_requires_java() -> None: + """Launch command construction must fail fast when Java is missing.""" + from solidlsp.language_servers import bsl_language_server + from solidlsp.language_servers.bsl_language_server import BSLLanguageServer + + settings = SolidLSPSettings() + settings.ls_specific_settings[Language.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"} + provider = BSLLanguageServer.DependencyProvider( + settings.get_ls_specific_settings(Language.BSL), + "/tmp/ls_resources", + ) + + with mock.patch.object(bsl_language_server.shutil, "which", return_value=None): + with pytest.raises(RuntimeError, match="not found on PATH"): + provider.create_launch_command() + + +def test_bsl_launch_command_rejects_old_java() -> None: + """Java older than the minimum supported major version must be rejected up front.""" + from solidlsp.language_servers import bsl_language_server + from solidlsp.language_servers.bsl_language_server import BSL_LS_MIN_JAVA_VERSION, BSLLanguageServer + + settings = SolidLSPSettings() + settings.ls_specific_settings[Language.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"} + provider = BSLLanguageServer.DependencyProvider( + settings.get_ls_specific_settings(Language.BSL), + "/tmp/ls_resources", + ) + + with ( + mock.patch.object(bsl_language_server.shutil, "which", return_value="/usr/bin/java"), + mock.patch.object(bsl_language_server, "_get_java_major_version", return_value=BSL_LS_MIN_JAVA_VERSION - 1), + ): + with pytest.raises(RuntimeError, match=f"Java {BSL_LS_MIN_JAVA_VERSION}\\+"): + provider.create_launch_command() diff --git a/test/solidlsp/bsl/test_bsl_diagnostics.py b/test/solidlsp/bsl/test_bsl_diagnostics.py new file mode 100644 index 00000000..7974b4a6 --- /dev/null +++ b/test/solidlsp/bsl/test_bsl_diagnostics.py @@ -0,0 +1,21 @@ +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.bsl +@pytest.mark.skipif(shutil.which("java") is None, reason="Java 21+ is required for BSL LSP") +class TestBSLDiagnostics: + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None: + """bsl-language-server must flag the unterminated string literal in the fixture.""" + assert_file_diagnostics( + language_server, + "diagnostics_sample.bsl", + (), + min_count=1, + )