Skip to content
38 changes: 38 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ plugins {
`java-library`
`maven-publish`
jacoco
id("org.graalvm.buildtools.native") version "1.1.0"
id("com.diffplug.spotless") version "7.0.4"
id("me.qoomon.git-versioning") version "6.4.4"
id("io.freefair.lombok") version "9.5.0"
Expand Down Expand Up @@ -100,20 +101,43 @@ dependencies {
exclude("commons-logging", "commons-logging")
exclude("com.sun.xml.bind", "jaxb-core")
exclude("com.sun.xml.bind", "jaxb-impl")
// io.grpc.netty-shaded ломает native-image: его static init захватывает
// ch.qos.logback.classic.Logger в image heap. LanguageTool сам netty-shaded
// не использует — только в составе grpc-stub-а для удалённых правил, который
// мы не задействуем; остальные grpc-api/grpc-protobuf остаются на classpath,
// чтобы LanguageTool мог разрешать `io.grpc.Channel` при загрузке правил.
exclude("io.grpc", "grpc-netty-shaded")
}
implementation("org.languagetool:language-en:$languageToolVersion") {
exclude("commons-logging:commons-logging")
exclude("com.sun.xml.bind", "jaxb-core")
exclude("com.sun.xml.bind", "jaxb-impl")
// io.grpc.netty-shaded ломает native-image: его static init захватывает
// ch.qos.logback.classic.Logger в image heap. LanguageTool сам netty-shaded
// не использует — только в составе grpc-stub-а для удалённых правил, который
// мы не задействуем; остальные grpc-api/grpc-protobuf остаются на classpath,
// чтобы LanguageTool мог разрешать `io.grpc.Channel` при загрузке правил.
exclude("io.grpc", "grpc-netty-shaded")
Comment on lines 111 to +120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Gradle Kotlin DSL, for ModuleDependency.exclude inside a dependency block, is exclude("commons-logging:commons-logging")valid syntax to exclude groupcommons-loggingand modulecommons-logging, or should it use separate groupandmodule arguments?

💡 Result:

For Gradle Kotlin DSL, inside a dependency block using ModuleDependency.exclude, you should use separate group and module arguments (i.e., exclude(group =..., module =...)); the single-string coordinate syntax exclude("commons-logging:commons-logging") is not the supported ModuleDependency.exclude overload for per-dependency transitive exclusions. Gradle’s Kotlin DSL documentation shows the per-dependency exclusion rule as: exclude(group = "commons-collections", module = "commons-collections") [1] The ModuleDependency Kotlin DSL API also defines exclude with an optional group and module parameters: fun <T: ModuleDependency> T.exclude(group: String? = null, module: String? = null): T [2] So for commons-logging, use: implementation("...") { exclude(group = "commons-logging", module = "commons-logging") } If you try to use a coordinate-style string like exclude("group:module"), it may not match the available Kotlin DSL signature for ModuleDependency.exclude, and you can hit a “Could not find method exclude for arguments [...]” error (reported for similar usage) [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the language-en dependency block area
echo "---- language-en block (lines ~100-140) ----"
nl -ba build.gradle.kts | sed -n '90,150p'

# Show any other LanguageTool language dependency blocks nearby for exclude call shape comparison
echo
echo "---- language tool dependency blocks (language-*) ----"
rg -n 'org\.languagetool:language-' build.gradle.kts

# Print the blocks for languagetool-core / language-ru / language-en / language-de (if present)
for k in "languagetool-core" "language-ru" "language-en" "language-de"; do
  echo
  echo "---- searching for $k ----"
  # Print ~25 lines around each match
  rg -n "$k" build.gradle.kts | cut -d: -f1 | uniq | while read -r ln; do
    start=$((ln-15))
    end=$((ln+30))
    nl -ba build.gradle.kts | sed -n "${start},${end}p"
  done
done

# Locate graalvmNative block and inspect it
echo
echo "---- graalvmNative block ----"
rg -n 'graalvmNative' build.gradle.kts || true
# Print a window around the first occurrence
first=$(rg -n 'graalvmNative' build.gradle.kts | head -n1 | cut -d: -f1 || true)
if [[ -n "${first:-}" ]]; then
  nl -ba build.gradle.kts | sed -n "$((first-30)),$((first+80))p"
fi

# Look for any explicit GraalVM/toolchain pinning knobs
echo
echo "---- possible toolchain pinning knobs ----"
rg -n 'GRAAL|graalvm|toolchain|vendor|CE|EE|javaToolchain|toolchain|graalvmNative\.config|nativeImage' build.gradle.kts || true

Repository: 1c-syntax/bsl-language-server

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="build.gradle.kts"
if [[ ! -f "$file" ]]; then
  echo "Missing $file"
  exit 1
fi

echo "---- language-en block around first match ----"
line=$(rg -n 'org\.languagetool:language-en' "$file" | head -n1 | cut -d: -f1 || true)
if [[ -n "${line:-}" ]]; then
  start=$((line-20))
  end=$((line+60))
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d  %s\n", NR, $0}' "$file"
else
  echo "No org.languagetool:language-en match found"
fi

echo
echo "---- language tool dependency blocks (language-*) ----"
rg -n 'org\.languagetool:language-' "$file" || true

echo
for k in "languagetool-core" "language-ru" "language-en" "language-de"; do
  echo
  echo "---- windows around $k ----"
  rg -n "$k" "$file" || true
done

echo
echo "---- graalvmNative block ----"
graal_line=$(rg -n 'graalvmNative' "$file" | head -n1 | cut -d: -f1 || true)
if [[ -n "${graal_line:-}" ]]; then
  start=$((graal_line-40))
  end=$((graal_line+120))
  awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%6d  %s\n", NR, $0}' "$file"
else
  echo "No graalvmNative match found"
fi

echo
echo "---- possible toolchain pinning knobs ----"
rg -n 'GRAAL|graalvm|toolchain|vendor|CE|EE|javaToolchain|graalvmNative\.config|nativeImage' "$file" || true

Repository: 1c-syntax/bsl-language-server

Length of output: 12339


Fix malformed commons-logging exclude in language-en

  • implementation("org.languagetool:language-en:$languageToolVersion") uses exclude("commons-logging:commons-logging"), which is the wrong form; it should exclude with separate group and module like the neighboring languagetool-core / language-ru blocks.
  • Consider encoding/pinning the GraalVM toolchain/version for graalvmNative builds (the file only sets mainClass and buildArgs), since the referenced GraalVM CE version isn’t reflected in Gradle config.
Suggested fix
     implementation("org.languagetool:language-en:$languageToolVersion") {
-        exclude("commons-logging:commons-logging")
+        exclude(group = "commons-logging", module = "commons-logging")
         exclude("com.sun.xml.bind", "jaxb-core")
         exclude("com.sun.xml.bind", "jaxb-impl")
         // io.grpc.netty-shaded ломает native-image: его static init захватывает
         // ch.qos.logback.classic.Logger в image heap. LanguageTool сам netty-shaded
         // не использует — только в составе grpc-stub-а для удалённых правил, который
         // мы не задействуем; остальные grpc-api/grpc-protobuf остаются на classpath,
         // чтобы LanguageTool мог разрешать `io.grpc.Channel` при загрузке правил.
         exclude("io.grpc", "grpc-netty-shaded")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
implementation("org.languagetool:language-en:$languageToolVersion") {
exclude("commons-logging:commons-logging")
exclude("com.sun.xml.bind", "jaxb-core")
exclude("com.sun.xml.bind", "jaxb-impl")
// io.grpc.netty-shaded ломает native-image: его static init захватывает
// ch.qos.logback.classic.Logger в image heap. LanguageTool сам netty-shaded
// не использует — только в составе grpc-stub-а для удалённых правил, который
// мы не задействуем; остальные grpc-api/grpc-protobuf остаются на classpath,
// чтобы LanguageTool мог разрешать `io.grpc.Channel` при загрузке правил.
exclude("io.grpc", "grpc-netty-shaded")
implementation("org.languagetool:language-en:$languageToolVersion") {
exclude(group = "commons-logging", module = "commons-logging")
exclude("com.sun.xml.bind", "jaxb-core")
exclude("com.sun.xml.bind", "jaxb-impl")
// io.grpc.netty-shaded ломает native-image: его static init захватывает
// ch.qos.logback.classic.Logger в image heap. LanguageTool сам netty-shaded
// не использует — только в составе grpc-stub-а для удалённых правил, который
// мы не задействуем; остальные grpc-api/grpc-protobuf остаются на classpath,
// чтобы LanguageTool мог разрешать `io.grpc.Channel` при загрузке правил.
exclude("io.grpc", "grpc-netty-shaded")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.gradle.kts` around lines 111 - 120, The exclude for commons-logging in
the implementation("org.languagetool:language-en:$languageToolVersion") block is
malformed; change exclude("commons-logging:commons-logging") to the correct
group/module form (e.g., exclude(group="commons-logging",
module="commons-logging") or exclude("commons-logging", "commons-logging")) to
match the neighboring languagetool-core/language-ru excludes, and ensure the
same pattern is used for other excludes like io.grpc:grpc-netty-shaded;
additionally, pin the GraalVM CE version/toolchain used by the graalvmNative
configuration (currently only mainClass and buildArgs are set) by adding an
explicit toolchain or version setting in the graalvmNative/Gradle toolchain
configuration so native-image builds use the intended GraalVM release.

}
implementation("org.languagetool:language-ru:$languageToolVersion") {
exclude("commons-logging", "commons-logging")
exclude("com.sun.xml.bind", "jaxb-core")
exclude("com.sun.xml.bind", "jaxb-impl")
// io.grpc.netty-shaded ломает native-image: его static init захватывает
// ch.qos.logback.classic.Logger в image heap. LanguageTool сам netty-shaded
// не использует — только в составе grpc-stub-а для удалённых правил, который
// мы не задействуем; остальные grpc-api/grpc-protobuf остаются на classpath,
// чтобы LanguageTool мог разрешать `io.grpc.Channel` при загрузке правил.
exclude("io.grpc", "grpc-netty-shaded")
}

// AOP
implementation("org.aspectj:aspectjrt:1.9.25.1")
// aspectjweaver нужен Spring AOP в рантайме для парсинга pointcut-выражений у @Aspect-бинов
// из транзитивных зависимостей (Sentry CheckIn / Spring caching advisor / etc.).
// Наши собственные аспекты по-прежнему вплетаются в байт-код через CTW
// (io.freefair.aspectj.post-compile-weaving), runtime-Spring AOP их не проксирует.
implementation("org.aspectj:aspectjweaver:1.9.25.1")

// commons utils
implementation("commons-io:commons-io:2.22.0")
Expand Down Expand Up @@ -200,6 +224,20 @@ tasks.named<org.springframework.boot.gradle.tasks.bundling.BootBuildImage>("boot
imageName.set("docker.io/1csyntax/bsl-language-server:${project.version}")
}

graalvmNative {
binaries {
named("main") {
sharedLibrary.set(false)
mainClass.set("com.github._1c_syntax.bsl.languageserver.BSLLSPLauncher")
buildArgs.addAll(
"--no-fallback",
"-H:+UnlockExperimentalVMOptions",
"-H:+ReportExceptionStackTraces"
)
}
}
}

afterEvaluate {
tasks.named("spotlessJavaCheck") {
dependsOn(tasks.generateSentryDebugMetaPropertiesjava)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ private static String readVersion() {
.getContextClassLoader()
.getResourceAsStream("META-INF/MANIFEST.MF");
Comment on lines 58 to 60

if (mfStream == null) {
// В native-image MANIFEST.MF не упакован — обходимся пакетной версией.
var pkgVersion = AutoServerInfo.class.getPackage().getImplementationVersion();
return pkgVersion != null ? pkgVersion : "";
}

Manifest manifest = new Manifest();
try {
manifest.read(mfStream);
Expand All @@ -67,7 +73,8 @@ private static String readVersion() {
return "";
}

return manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
var version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
return version != null ? version : "";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,20 @@
import com.github._1c_syntax.bsl.languageserver.context.DocumentContext;
import com.github._1c_syntax.bsl.languageserver.context.FileType;
import com.github._1c_syntax.bsl.languageserver.events.LanguageServerInitializeRequestReceivedEvent;
import com.github._1c_syntax.utils.Absolute;
import lombok.RequiredArgsConstructor;
import org.eclipse.lsp4j.ClientInfo;
import org.eclipse.lsp4j.InitializeParams;
import org.jspecify.annotations.Nullable;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.event.EventListener;

import java.net.URI;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@CacheConfig(cacheNames = "testSources")
public abstract class AbstractRunTestsCodeLensSupplier<T extends CodeLensData>
implements CodeLensSupplier<T> {

protected final LanguageServerConfiguration configuration;
protected final TestSourcesProvider testSourcesProvider;

private boolean clientIsSupported;

Expand All @@ -59,27 +51,24 @@ public abstract class AbstractRunTestsCodeLensSupplier<T extends CodeLensData>
* @param event Событие
*/
@EventListener
@CacheEvict(allEntries = true)
public void handleEvent(LanguageServerInitializeRequestReceivedEvent event) {
var clientName = Optional.of(event)
.map(LanguageServerInitializeRequestReceivedEvent::getParams)
.map(InitializeParams::getClientInfo)
.map(ClientInfo::getName)
.orElse("");
clientIsSupported = "Visual Studio Code".equals(clientName);
testSourcesProvider.evict();
}

/**
* Обработчик события {@link LanguageServerConfigurationChangedEvent}.
* <p>
* Сбрасывает кеш при изменении конфигурации.
*
* @param event Событие
*/
@EventListener
@CacheEvict(allEntries = true)
public void handleLanguageServerConfigurationChange(LanguageServerConfigurationChangedEvent event) {
// No-op. Служит для сброса кеша при изменении конфигурации
@EventListener(LanguageServerConfigurationChangedEvent.class)
public void handleLanguageServerConfigurationChange() {
testSourcesProvider.evict();
}

/**
Expand All @@ -88,41 +77,13 @@ public void handleLanguageServerConfigurationChange(LanguageServerConfigurationC
@Override
public boolean isApplicable(DocumentContext documentContext) {
var uri = documentContext.getUri();
var testSources = getSelf().getTestSources(documentContext.getServerContext().getConfigurationRoot());
var testSources = testSourcesProvider.getTestSources(documentContext.getServerContext().getConfigurationRoot());

return clientIsSupported
&& documentContext.getFileType() == FileType.OS
&& testSources.stream().anyMatch(testSource -> isInside(uri, testSource));
}

/**
* Получить self-injected экземпляр себя для работы механизмов кэширования.
*
* @return Управляемый Spring'ом экземпляр себя
*/
protected abstract AbstractRunTestsCodeLensSupplier<T> getSelf();

/**
* Получить список каталогов с тестами с учетом корня рабочей области.
* <p>
* public для работы @Cachable.
*
* @param configurationRoot Корень конфигурации
* @return Список исходных файлов тестов
*/
@Cacheable
public Set<URI> getTestSources(@Nullable Path configurationRoot) {
var configurationRootString = Optional.ofNullable(configurationRoot)
.map(Path::toString)
.orElse("");

return configuration.getCodeLensOptions().getTestRunnerAdapterOptions().getTestSources()
.stream()
.map(testDir -> Path.of(configurationRootString, testDir))
.map(path -> Absolute.path(path).toUri())
.collect(Collectors.toSet());
}

private static boolean isInside(URI childURI, URI parentURI) {
return !parentURI.relativize(childURI).isAbsolute();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,11 @@
import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol;
import com.github._1c_syntax.bsl.languageserver.utils.Resources;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Command;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

Expand All @@ -58,19 +55,13 @@ public class DebugTestCodeLensSupplier
private final TestRunnerAdapter testRunnerAdapter;
private final Resources resources;

// Self-injection для работы кэша в базовом классе.
@Autowired
@Lazy
@Getter
@SuppressWarnings("NullAway.Init")
private DebugTestCodeLensSupplier self;

public DebugTestCodeLensSupplier(
LanguageServerConfiguration configuration,
TestSourcesProvider testSourcesProvider,
TestRunnerAdapter testRunnerAdapter,
Resources resources
) {
super(configuration);
super(configuration, testSourcesProvider);
this.testRunnerAdapter = testRunnerAdapter;
this.resources = resources;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,9 @@
import com.github._1c_syntax.bsl.languageserver.context.DocumentContext;
import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol;
import com.github._1c_syntax.bsl.languageserver.utils.Resources;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Command;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

Expand All @@ -54,18 +51,13 @@ public class RunAllTestsCodeLensSupplier
private final TestRunnerAdapter testRunnerAdapter;
private final Resources resources;

// Self-injection для работы кэша в базовом классе.
@Autowired
@Lazy
@Getter
private RunAllTestsCodeLensSupplier self;

public RunAllTestsCodeLensSupplier(
LanguageServerConfiguration configuration,
TestSourcesProvider testSourcesProvider,
TestRunnerAdapter testRunnerAdapter,
Resources resources
) {
super(configuration);
super(configuration, testSourcesProvider);
this.testRunnerAdapter = testRunnerAdapter;
this.resources = resources;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,11 @@
import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol;
import com.github._1c_syntax.bsl.languageserver.utils.Resources;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.lsp4j.CodeLens;
import org.eclipse.lsp4j.Command;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

Expand All @@ -61,19 +58,13 @@ public class RunTestCodeLensSupplier
private final TestRunnerAdapter testRunnerAdapter;
private final Resources resources;

// Self-injection для работы кэша в базовом классе.
@Autowired
@Lazy
@Getter
@SuppressWarnings("NullAway.Init")
private RunTestCodeLensSupplier self;

public RunTestCodeLensSupplier(
LanguageServerConfiguration configuration,
TestSourcesProvider testSourcesProvider,
TestRunnerAdapter testRunnerAdapter,
Resources resources
) {
super(configuration);
super(configuration, testSourcesProvider);
this.testRunnerAdapter = testRunnerAdapter;
this.resources = resources;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* This file is a part of BSL Language Server.
*
* Copyright (c) 2018-2026
* Alexey Sosnoviy <labotamy@gmail.com>, Nikita Fedkin <nixel2007@gmail.com> and contributors
*
* SPDX-License-Identifier: LGPL-3.0-or-later
*
* BSL Language Server is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* BSL Language Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with BSL Language Server.
*/
package com.github._1c_syntax.bsl.languageserver.codelenses;

import com.github._1c_syntax.bsl.languageserver.configuration.LanguageServerConfiguration;
import com.github._1c_syntax.utils.Absolute;
import lombok.RequiredArgsConstructor;
import org.jspecify.annotations.Nullable;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Поставщик списка каталогов с тестовыми исходниками для линз запуска тестов.
* <p>
* Кэширование вынесено в отдельный бин, чтобы вызов кэшируемого метода
* шёл через нормальный Spring AOP-прокси без self-injection в линзах
* (паттерн {@code @Lazy self} ломает native-image на конфликте
* pre-generated CGLIB-стабов с runtime AOP).
*/
@Component
@CacheConfig(cacheNames = "testSources")
@RequiredArgsConstructor
public class TestSourcesProvider {

private final LanguageServerConfiguration configuration;

/**
* Получить список каталогов с тестами с учётом корня рабочей области.
*
* @param configurationRoot Корень конфигурации.
* @return URI каталогов с тестами.
*/
@Cacheable
public Set<URI> getTestSources(@Nullable Path configurationRoot) {
var configurationRootString = Optional.ofNullable(configurationRoot)
.map(Path::toString)
.orElse("");

return configuration.getCodeLensOptions().getTestRunnerAdapterOptions().getTestSources()
.stream()
.map(testDir -> Path.of(configurationRootString, testDir))
.map(path -> Absolute.path(path).toUri())
.collect(Collectors.toSet());
}

/**
* Сбросить кэш каталогов с тестами.
*/
@CacheEvict(allEntries = true)
public void evict() {
// No-op. Сброс кеша выполняется аспектом @CacheEvict.
}
}
Loading
Loading