build(native): initial GraalVM native-image support#3938
Conversation
Task :nativeCompile now builds a binary. Key changes: - Apply org.graalvm.buildtools.native plugin with executable build type and explicit mainClass for BSLLSPLauncher. - Route aspectjweaver через отдельную aotProcessorClasspath-конфигурацию, чтобы он попадал в Spring AOT-процессор (ему нужен PointcutParser для @Aspect-бинов), но не оседал в рантайм/bootJar — CTW по-прежнему обеспечивает io.freefair.aspectj.post-compile-weaving. - Exclude транзитивные io.grpc.netty-shaded из LanguageTool: они тянут Logger в image heap через netty static init и валят сборку. - Ship reachability-metadata.json для $$SpringCGLIB$$1-классов наших workspace-scoped прокси — Spring AOT регистрирует им только конструкторы, native-image требует доступ к статическому полю CGLIB$FACTORY_DATA. - AutoServerInfo.readVersion: при отсутствии META-INF/MANIFEST.MF (типичный кейс native-image) больше не падает NPE, а возвращает пакетную версию или пустую строку. Известное ограничение: бинарь успешно собирается, но контекст Spring падает на инициализации beans из-за конфликта pre-generated CGLIB-стабов с runtime-AOP (Spring caching + @lazy self-injection). Полностью работающий native-runtime потребует отдельной работы — рефактора self-injection-паттернов и расширенных RuntimeHints. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds GraalVM native-image build configuration and reachability metadata, provides a manifest-version fallback for native runtimes, refactors CodeLens test-source lookup into a cached TestSourcesProvider used by suppliers/tests, replaces ChangesGraalVM native-image support
TestSourcesProvider and CodeLens wiring
Document/Server context explicit wiring
Diagnostics provider change
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds initial Gradle/GraalVM native-image build support for BSL Language Server by making Spring AOT / native-image compilation succeed without changing the runtime/bootJar classpath.
Changes:
- Adds the
org.graalvm.buildtools.nativeGradle plugin and configures thenativeCompilebinary (main class + native-image args). - Adjusts dependencies to unblock AOT/native-image (AOT-only
aspectjweaverclasspath; excludes LanguageTool’s transitive gRPC Netty artifacts). - Adds native-image reachability metadata for Spring AOT generated workspace-scoped CGLIB proxies and hardens version detection for native-image (manifest stream can be null).
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
build.gradle.kts |
Adds GraalVM native build plugin/config, AOT-only aspectjweaver classpath, and excludes problematic transitive gRPC deps. |
src/main/java/com/github/_1c_syntax/bsl/languageserver/AutoServerInfo.java |
Prevents NPE in native-image when MANIFEST.MF is absent; returns a safe version string. |
src/main/resources/META-INF/native-image/io.github.1c-syntax/bsl-language-server-hints/reachability-metadata.json |
Adds reflection metadata for Spring AOT-generated $$SpringCGLIB$$1 proxies to prevent native-image failures. |
| final InputStream mfStream = Thread.currentThread() | ||
| .getContextClassLoader() | ||
| .getResourceAsStream("META-INF/MANIFEST.MF"); |
| "type": "com.github._1c_syntax.bsl.languageserver.configuration.LanguageServerConfiguration$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.diagnostics.infrastructure.DiagnosticInfos$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.references.model.AnnotationRepository$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.references.model.LocationRepository$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.references.model.SymbolOccurrenceRepository$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.types.TypeService$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.types.index.SymbolTypeIndex$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.types.inferencer.ExpressionTypeInferencer$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.types.oscript.OScriptLibraryIndex$$SpringCGLIB$$1", | ||
| "allDeclaredFields": true, | ||
| "allDeclaredConstructors": true, | ||
| "allDeclaredMethods": true | ||
| }, | ||
| { | ||
| "type": "com.github._1c_syntax.bsl.languageserver.types.oscript.OScriptModuleMembersProvider$$SpringCGLIB$$1", |
Test Results1 422 files - 1 422 1 422 suites - 1 422 40m 47s ⏱️ - 29m 7s For more details on these failures, see this check. Results for commit 0c8f84d. ± Comparison against base commit 01c87bc. ♻️ This comment has been updated with latest results. |
…jection В `*CodeLensSupplier`-ах `@Lazy @Autowired self` использовался только чтобы позвать `@Cacheable getTestSources(Path)` из базового класса через Spring AOP-прокси. Под native-image этот паттерн ломается: `@Lazy`-инъекция создаёт runtime CGLIB-прокси через `ContextAnnotationAutowireCandidateResolver. buildLazyResolutionProxy`, который конфликтует с pre-generated CGLIB-стабом от Spring AOT (ClassCast: `CglibAopProxy$SerializableNoOp` → `Dispatcher` в `setCallbacks`). Выношу кэширование в отдельный `TestSourcesProvider`: - `@Component @cAcHecONFig(cacheNames = "testSources")` с `@Cacheable getTestSources(Path)` и `@CacheEvict allEntries=true evict()`. - `AbstractRunTestsCodeLensSupplier` принимает `TestSourcesProvider` через конструктор, избавляется от `@Cacheable`/`@CacheConfig`/`getSelf()`. Обработчики событий явно дёргают `testSourcesProvider.evict()` (раньше это делал `@CacheEvict` no-op). - `DebugTestCodeLensSupplier`/`RunTestCodeLensSupplier`/`RunAllTestsCodeLensSupplier` больше не держат self-поля и лишние импорты `@Lazy`/`@Autowired`/`Lombok.Getter`, прокидывают `TestSourcesProvider` в `super(...)`. - `AbstractRunTestsCodeLensSupplierTest.TestConfig` обновлён под новый конструктор. Кэш по-прежнему живёт в Spring (через нормальный AOP-прокси на отдельном бине, а не self-injection-через-`@Lazy`). Юнит-тесты Debug/Run/RunAllTests/AbstractRunTests по-прежнему зелёные. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Дополнения после рефактора `@Lazy self` в линзах: - `aspectjweaver` снова в `implementation`. Раньше его прятали в отдельный `aotProcessorClasspath`, чтобы рантайм-`Spring AOP` не активировался и не плодил CGLIB-прокси, конфликтующие с pre-generated стабами Spring AOT. После выноса кэша линз в `TestSourcesProvider` `@Lazy`-self-инъекций больше нет, а Sentry CheckIn / Spring caching advisor всё равно требуют `AspectJExpressionPointcut` (он лежит в weaver-jar) при runtime-инициализации бинов. Так что weaver возвращается, отдельная aot-only конфигурация удаляется, CTW для наших аспектов продолжает работать через CTW-плагин freefair. - В `reachability-metadata.json` добавляется reflection для `aspectOf()`-методов наших трёх CTW-аспектов (`EventPublisherAspect`, `SentryAspect`, `MeasuresAspect`) — без этого Spring `AspectJConfiguration.*` падает `MissingReflectionRegistrationError: aspectOf()`. - В тот же файл выгружается перечень ресурс-бандлов (`bundles`), собранный по `src/main/resources/**/*_(en|ru).properties`, чтобы `ResourceBundle.getBundle(class.getName(), ...)` не получал `MissingResourceException` в native-runtime. С этими хинтами Spring-контекст в native поднимается дальше: успешно инициализируются Spring AOP (cache-advisor, sentry checkin/exception), наши `@Aspect`-бины, регистрируются ресурс-бандлы. Следующий блокер — LSP4J JSON-RPC адаптеры (`org.eclipse.lsp4j.adapters.*` требуют no-arg конструктор через reflection); это уже отдельный пласт работ по регистрации reflection для LSP4J. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Обновление: ещё две итерации — теперь Spring-контекст в native поднимается значительно дальше. Что докинул:
Текущий статус бинаря: контекст уже инициализирует Spring AOP, sentry checkin/exception advisor'ы, наши CTW-аспекты, ресурс-бандлы. Следующий блокер при LSP4J активно использует reflection для JSON-RPC сериализации ( Сравнение производительности native vs |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/AbstractRunTestsCodeLensSupplier.java (1)
69-72: ⚡ Quick winLet
TestSourcesProviderown configuration-change eviction.Right now every test CodeLens supplier clears the same
testSourcescache onLanguageServerConfigurationChangedEvent, so the cache is evicted N times and any new consumer has to remember to duplicate this hook. Moving this listener ontoTestSourcesProvider.evict()keeps invalidation with the cache-owning bean.🤖 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 `@src/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/AbstractRunTestsCodeLensSupplier.java` around lines 69 - 72, Move the configuration-change listener responsibility into the cache-owning bean: remove the `@EventListener` method handleLanguageServerConfigurationChange() from AbstractRunTestsCodeLensSupplier (and any other CodeLens suppliers) so they no longer call testSourcesProvider.evict() directly on LanguageServerConfigurationChangedEvent, and instead add an `@EventListener`(LanguageServerConfigurationChangedEvent.class) annotation to a new or existing method on TestSourcesProvider that calls evict(); keep the evict() method name and ensure TestSourcesProvider is a Spring bean so the listener runs there, thereby centralizing cache invalidation in TestSourcesProvider.evict().src/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/TestSourcesProvider.java (1)
66-70: ⚡ Quick winReturn an immutable set from the cache.
Collectors.toSet()caches a mutableSet, so one accidental mutation by any future caller would poison the shared cached value for every supplier. Prefer an immutable result here.♻️ Proposed fix
- .collect(Collectors.toSet()); + .collect(Collectors.toUnmodifiableSet());Based on learnings: prefer using Collections.emptyList() or an unmodifiable list to communicate read-only intent while maintaining the non-null contract.
🤖 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 `@src/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/TestSourcesProvider.java` around lines 66 - 70, The cached supplier currently collects into a mutable Set using Collectors.toSet(), which can be mutated by callers and corrupt the shared cache; update the collection step in TestSourcesProvider (the return of configuration.getCodeLensOptions().getTestRunnerAdapterOptions().getTestSources()... stream) to produce an immutable Set instead (for example use Collectors.toUnmodifiableSet() or wrap the result with Collections.unmodifiableSet via Collectors.collectingAndThen) so the cached value cannot be changed by downstream code.
🤖 Prompt for all review comments with 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.
Nitpick comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/AbstractRunTestsCodeLensSupplier.java`:
- Around line 69-72: Move the configuration-change listener responsibility into
the cache-owning bean: remove the `@EventListener` method
handleLanguageServerConfigurationChange() from AbstractRunTestsCodeLensSupplier
(and any other CodeLens suppliers) so they no longer call
testSourcesProvider.evict() directly on LanguageServerConfigurationChangedEvent,
and instead add an `@EventListener`(LanguageServerConfigurationChangedEvent.class)
annotation to a new or existing method on TestSourcesProvider that calls
evict(); keep the evict() method name and ensure TestSourcesProvider is a Spring
bean so the listener runs there, thereby centralizing cache invalidation in
TestSourcesProvider.evict().
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/TestSourcesProvider.java`:
- Around line 66-70: The cached supplier currently collects into a mutable Set
using Collectors.toSet(), which can be mutated by callers and corrupt the shared
cache; update the collection step in TestSourcesProvider (the return of
configuration.getCodeLensOptions().getTestRunnerAdapterOptions().getTestSources()...
stream) to produce an immutable Set instead (for example use
Collectors.toUnmodifiableSet() or wrap the result with
Collections.unmodifiableSet via Collectors.collectingAndThen) so the cached
value cannot be changed by downstream code.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 74af6e22-6ef6-449c-8d67-2ef2716a5b15
📒 Files selected for processing (8)
build.gradle.ktssrc/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/AbstractRunTestsCodeLensSupplier.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/DebugTestCodeLensSupplier.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/RunAllTestsCodeLensSupplier.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/RunTestCodeLensSupplier.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/codelenses/TestSourcesProvider.javasrc/main/resources/META-INF/native-image/io.github.1c-syntax/bsl-language-server-hints/reachability-metadata.jsonsrc/test/java/com/github/_1c_syntax/bsl/languageserver/codelenses/AbstractRunTestsCodeLensSupplierTest.java
Существенный шаг вперёд. Бинарь успешно стартует, поднимает Spring-контекст,
парсит CLI-аргументы picocli, доходит до AnalyzeCommand.call() и начинает
обход исходников.
Что изменилось в коде:
- `DiagnosticComputer`: `@Lookup("diagnostics")` заменён на `ObjectProvider<List<BSLDiagnostic>>`.
Spring `@Lookup` создаёт CGLIB-сабкласс на абстрактном классе во время выполнения
через `CglibSubclassingInstantiationStrategy`, что несовместимо с AOT —
native-image не находит no-arg конструктор у AOT-сгенерированного `$$SpringCGLIB$$0`.
`ObjectProvider.getObject(documentContext)` даёт тот же эффект (per-call prototype
бин с аргументом), но идёт через обычный Spring DI без runtime-проксирования.
- В `build.gradle.kts`: возвращены `grpc-api`/`grpc-protobuf`/`grpc-stub` —
`LanguageTool` использует `io.grpc.Channel` при загрузке правил, без них и JVM-сборка
падает `ClassNotFoundException`. Из `netty-shaded` оставлен ровно тот один артефакт,
чей static init ломает native-image (`ch.qos.logback.classic.Logger` в image heap).
- `aspectjweaver` снова в `implementation`. После рефакторинга `@Lazy self`-инъекций
в линзах (предыдущий коммит) runtime-Spring AOP больше не конфликтует с
pre-generated CGLIB-стабами AOT, поэтому отдельный `aotProcessorClasspath`
не нужен — наши аспекты по-прежнему вплетены через CTW, а Sentry-AOP
и Spring-cache-advisor пользуются weaver-ом на старте.
Что в `reachability-metadata.json`:
Прогнал native-image-agent на полном JVM-`analyze` против `ssl_3_2/src/cf`
и слил собранные хинты с нашими вручную написанными:
- 1876 reflection-записей (было 25): покрывают LSP4J POJO/адаптеры, Sentry,
Spring AOT CGLIB-стабы scoped-прокси, Jackson, многочисленные прототип-бины
и т.д. Сохранены ранее проставленные `aspectOf()`-методы для CTW-аспектов.
- 916 resource-записей: ресурсные файлы LanguageTool, locale-файлы,
Spring meta-info и т.п.
- 205 ResourceBundle (`bundles`) — все `src/main/resources/**/*_(en|ru).properties`.
- Добавлены `unsafeAllocated` для 22 array-типов конвертеров
`org.apache.commons.beanutils.ConvertUtilsBean` (`Calendar[]`, `Date[]`,
`Boolean[]`/`Integer[]`/… и примитивные массивы) — без них падал
`LanguageServerConfiguration.captureInitialSnapshot()`.
Что пока не работает:
`analyze` против 1C-конфигурации падает на парсинге `Configuration.xml`/`Configurations.xml`:
`com.github._1c_syntax.bsl.reader.common.xstream.ExtendXStream#registerConverters`
(транзитивно из `io.github.1c-syntax:mdclasses:0.18.0`) использует `io.github.classgraph:ClassGraph#scan`
для рантайм-поиска XStream-конвертеров через `acceptPackages(...)`. ClassGraph
во время scan'а пытается прочитать boot-модули JDK через JRT — а в native-image
JRT-файлсистема отключена (`UnsupportedFeatureError: JRT file system is disabled`).
Это блокер на стороне внешнего `mdclasses`: либо upstream-патч (зарегистрировать
все конвертеры статически, без рантайм-скана), либо `SVM substitution`.
Дальнейший прогресс по этому фронту требует отдельной работы.
Текущий статус по командам:
- `bsl-language-server --version` / `version` — OK
- `bsl-language-server --help` — OK
- `bsl-language-server analyze --srcDir <1C-config>` — Spring up, picocli OK,
AnalyzeCommand.call() выполняется, падение на mdclasses ClassGraph (см. выше).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Прогресс — нативный бинарь теперь полноценно стартует и доходит до анализа. Что добавил последним коммитом (
Текущий статус native-бинаря:
Что нужно для финального шага (вне рамок этого PR):
Сравнение производительности native vs |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@build.gradle.kts`:
- Around line 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.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7521485d-2866-48e2-884a-86fb2e0d32da
📒 Files selected for processing (3)
build.gradle.ktssrc/main/java/com/github/_1c_syntax/bsl/languageserver/context/computer/DiagnosticComputer.javasrc/main/resources/META-INF/native-image/io.github.1c-syntax/bsl-language-server-hints/reachability-metadata.json
| 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") |
There was a problem hiding this comment.
🧩 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:
- 1: https://docs.gradle.org/nightly/userguide/how_to_exclude_transitive_dependencies.html
- 2: https://gradle.github.io/kotlin-dsl-docs/api/org.gradle.api.artifacts/-module-dependency/index.html
- 3: https://stackoverflow.com/questions/62868920/gradle-exclude-a-transitive-dependency
🏁 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 || trueRepository: 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" || trueRepository: 1c-syntax/bsl-language-server
Length of output: 12339
Fix malformed commons-logging exclude in language-en
implementation("org.languagetool:language-en:$languageToolVersion")usesexclude("commons-logging:commons-logging"), which is the wrong form; it should exclude with separategroupandmodulelike the neighboringlanguagetool-core/language-rublocks.- Consider encoding/pinning the GraalVM toolchain/version for
graalvmNativebuilds (the file only setsmainClassandbuildArgs), 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.
| 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.
…nalyze
Несколько крупных шагов вперёд по native-image — бинарь теперь поднимает
Spring-контекст, парсит CLI, читает 1C-метаданные через XStream и доходит
до фактического анализа bsl-файлов.
Что добавлено:
- **Shadow `ExtendXStream`** в `src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/`.
Оригинал в `io.github.1c-syntax:mdclasses:0.18.0` использует `io.github.classgraph:ClassGraph#scan()`
для рантайм-поиска `@CommonConverter`/`@DesignerConverter`/`@EDTConverter` и MD-классов
через `getClassesImplementing(MD.class)`. В native-image ClassGraph валится: его scan'у
нужно перечислить boot-модули JDK через JRT FS, который для native отключён по умолчанию,
а с `-H:+AllowJRTFileSystem` цепляется за `ImageReaderFactory.<clinit>` (`java.home` = null
→ `Path.of(null, "lib/modules")` NPE / `NoSuchFileException: lib/modules`). Это в любом
случае невыполнимо: в native-image нет рантайм-classpath JAR-ов, классы вкомпилированы
в бинарь, и динамический скан выдаёт пустоту.
Shadow-копия переопределяет два места, использующих ClassGraph (`registerConverters`
и `registerClasses`): в native-режиме (`ImageInfo.inImageRuntimeCode()`) идёт по
hardcoded-спискам конвертеров (17 common + 14 designer + 12 edt) и MD-классов (48),
выписанных из mdclasses 0.18.0. На JVM сохранена прежняя логика через ClassGraph —
идентичная оригиналу. Подменяется через стандартный classloader-precedence: наш
`build/classes/java/main` выше lib-jar'а в classpath.
Зависимости `compileOnly` для XStream/ClassGraph (доступны транзитивно через mdclasses
в рантайме) и `implementation` для `org.graalvm.sdk:nativeimage` (`ImageInfo` для
переключения JVM/native веток — артефакт лёгкий, в JVM возвращает false).
- **DocumentContext: ручная инициализация autowire-зависимостей.** Spring AOT в native-image
для prototype-бина, созданного через `ObjectProvider.getObject(args...)`, не выполняет
chained `andThen(__Autowiring::apply)` — `@Autowired`-сеттеры не вызываются и поля
остаются null. Подтверждено логированием в `@PostConstruct`: метод фигачит, а
`diagnosticComputer` = null. На JVM этого нет.
Заменяем `@Setter(onMethod_={@Autowired})` для 4 полей (`diagnosticComputer`,
`cognitiveComplexityComputerProvider`, `cyclomaticComplexityComputerProvider`,
`oScriptModuleTypeResolver`) на явный метод `initializeDependencies()`, вызываемый
из `ServerContext.createDocumentContext()` сразу после `documentContextProvider.getObject(uri, this)`.
Сами зависимости приходят в `ServerContext` через обычный constructor injection.
- **Расширенные reachability-хинты:** проектные ресурсы (`builtin-globals.json`,
`builtin-platform-types.json`, `parameters-schema.json`, `schema.json`,
`logback-spring.xml` и др.), глоб `org/languagetool/**` и `org/languagetool/resource/**`
для встроенных LT-словарей, дополнительные классы из реёх агентского прогона
на JVM-`analyze` (для CodeLensSupplier/DocumentContext полные allDeclared*-флаги).
- **`--initialize-at-run-time`** для `org.languagetool.{Languages,Language,JLanguageTool}`,
чтобы `Languages.<clinit>` не пытался читать ресурсы языковых модулей на build-time.
Текущее состояние:
- `./gradlew nativeCompile` — успех, бинарь ~140 MB.
- `version`/`--version`/`--help`/`format --help` — работают мгновенно.
- `analyze --srcDir <1C-config> --reporter sarif` — поднимает контекст, читает
Configuration.xml через XStream (без ClassGraph), создаёт DocumentContext'ы
с правильно инициализированными зависимостями, начинает обход файлов.
На пустой/минимальной директории успешно завершается и пишет SARIF-отчёт.
На полном ssl_3_2 (2175 файлов) пока упирается в TI taipout — нативный
бинарь работает медленнее, чем JVM (часть диагностик кидают NPE
per-file, но они catched в `DiagnosticComputer.internalCompute`). Анализ
бóльших проектов и оптимизация runtime — следующий шаг, но `nativeCompile`
уже доводится до конца.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ExtendXStream.java (2)
537-544: ⚡ Quick winUsing
getDeclaredConstructors()[0]is fragile for no-arg instantiation.Accessing
getDeclaredConstructors()[0]assumes the first declared constructor is the no-arg constructor, but the order of constructors returned is not guaranteed by the JVM specification. Consider usinggetDeclaredConstructor()(no parameters) to explicitly get the no-arg constructor.Suggested fix
private static Object instantiateNoArg(Class<?> clazz) { try { - return clazz.getDeclaredConstructors()[0].newInstance(); + return clazz.getDeclaredConstructor().newInstance(); - } catch (InvocationTargetException | IllegalAccessException | InstantiationException e) { + } catch (InvocationTargetException | IllegalAccessException | InstantiationException + | NoSuchMethodException e) { LOGGER.error("Cannot instantiate converter {}\n", clazz.getName(), e); throw new IllegalArgumentException("Cannot instantiate converter " + clazz.getName(), e); } }The same pattern appears in
getObjectsFromInfoClass()at lines 527-528.,
🤖 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 `@src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ExtendXStream.java` around lines 537 - 544, Replace fragile use of getDeclaredConstructors()[0] with an explicit no-arg constructor lookup: in instantiateNoArg and in getObjectsFromInfoClass, call clazz.getDeclaredConstructor() to obtain the no-arg Constructor, call constructor.setAccessible(true) if necessary, and invoke constructor.newInstance(); update exception handling to catch NoSuchMethodException in addition to the existing InvocationTargetException, IllegalAccessException, and InstantiationException, and log/throw the same IllegalArgumentException with the class name and original exception to preserve behavior.
378-385: 💤 Low valueJavadoc references non-existent constant.
The Javadoc at line 383 references
NATIVE_COMMON_CONVERTERSbut the actual field is namedNATIVE_CONVERTERS(line 246).Suggested fix
/** * Регистрирует конверторы нужного типа, фильтруя по пакету и аннотации. * <p> * На JVM выполняет рантайм-скан classpath через ClassGraph (поведение * оригинального mdclasses 0.18.0). В native-image идёт по статически - * известному списку из {`@link` `#NATIVE_COMMON_CONVERTERS`}, потому что + * известному списку из {`@link` `#NATIVE_CONVERTERS`}, потому что * ClassGraph валится на JRT-файлсистеме при попытке прочитать boot-модули JDK. */,
🤖 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 `@src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ExtendXStream.java` around lines 378 - 385, The Javadoc in class ExtendXStream incorrectly references a non-existent constant NATIVE_COMMON_CONVERTERS; update the Javadoc to reference the actual field name NATIVE_CONVERTERS used in this class (or rename the field if the intended symbol was different) so the Javadoc matches the existing code; specifically edit the Javadoc block that mentions NATIVE_COMMON_CONVERTERS to use NATIVE_CONVERTERS (symbol: ExtendXStream, NATIVE_CONVERTERS).src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java (1)
438-447: 💤 Low valueComment is outdated: the
@Autowiredsetters no longer exist.The comment states that in regular JVM mode this will "overwrite already-set values" from Spring setter injection. However, per the changes in
DocumentContext, the@Setter(onMethod_ =@Autowired)annotations have been removed—there are no setter methods for Spring to invoke anymore. In both JVM and native-image modes,initializeDependencies()is now the sole initialization path.Consider simplifying the comment to reflect the current design:
📝 Suggested comment update
- // Spring AOT в native-image не вызывает `@Autowired-setter`'ы для prototype-бина, - // созданного через ObjectProvider.getObject(args). Принудительно подкладываем - // нужные сервисы вручную; в обычной JVM это перезапишет уже выставленные значения - // теми же экземплярами (Spring-инъекция отрабатывает раньше). + // ObjectProvider.getObject(args) не запускает `@Autowired-сеттеры` для prototype-бинов + // ни в native-image, ни в JVM. Явно устанавливаем зависимости здесь.🤖 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 `@src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java` around lines 438 - 447, The comment above the call to documentContext.initializeDependencies(...) is outdated because DocumentContext no longer defines `@Setter`(onMethod_ = `@Autowired`) methods; remove or replace the existing paragraph and update it to a concise note that initializeDependencies is the single, intentional initialization path for both JVM and native-image (mention DocumentContext and documentContext.initializeDependencies to locate the code), e.g. state that Spring AOT/native-image requires manual injection via initializeDependencies so the method is always used for wiring dependencies.
🤖 Prompt for all review comments with 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.
Inline comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ExtendXStream.java`:
- Around line 500-504: getClassFromClassInfo is `@Nullable` and its return (clazz)
is passed into alias(simpleName, clazz) without a null check; modify the lambda
that iterates over ClassInfo (the .forEach block using clazzInfo, simpleName,
clazz) to guard against null: call getClassFromClassInfo(clazzInfo), and if it
returns null skip calling alias(simpleName, clazz) (optionally log or warn
including clazzInfo.getName() or simpleName) so alias never receives a null
class and no NPE/undefined behavior occurs.
---
Nitpick comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java`:
- Around line 438-447: The comment above the call to
documentContext.initializeDependencies(...) is outdated because DocumentContext
no longer defines `@Setter`(onMethod_ = `@Autowired`) methods; remove or replace the
existing paragraph and update it to a concise note that initializeDependencies
is the single, intentional initialization path for both JVM and native-image
(mention DocumentContext and documentContext.initializeDependencies to locate
the code), e.g. state that Spring AOT/native-image requires manual injection via
initializeDependencies so the method is always used for wiring dependencies.
In
`@src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ExtendXStream.java`:
- Around line 537-544: Replace fragile use of getDeclaredConstructors()[0] with
an explicit no-arg constructor lookup: in instantiateNoArg and in
getObjectsFromInfoClass, call clazz.getDeclaredConstructor() to obtain the
no-arg Constructor, call constructor.setAccessible(true) if necessary, and
invoke constructor.newInstance(); update exception handling to catch
NoSuchMethodException in addition to the existing InvocationTargetException,
IllegalAccessException, and InstantiationException, and log/throw the same
IllegalArgumentException with the class name and original exception to preserve
behavior.
- Around line 378-385: The Javadoc in class ExtendXStream incorrectly references
a non-existent constant NATIVE_COMMON_CONVERTERS; update the Javadoc to
reference the actual field name NATIVE_CONVERTERS used in this class (or rename
the field if the intended symbol was different) so the Javadoc matches the
existing code; specifically edit the Javadoc block that mentions
NATIVE_COMMON_CONVERTERS to use NATIVE_CONVERTERS (symbol: ExtendXStream,
NATIVE_CONVERTERS).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1cf31440-10d7-414f-abc8-1e0db969014f
📒 Files selected for processing (5)
build.gradle.ktssrc/main/java/com/github/_1c_syntax/bsl/languageserver/context/DocumentContext.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.javasrc/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ExtendXStream.javasrc/main/resources/META-INF/native-image/io.github.1c-syntax/bsl-language-server-hints/reachability-metadata.json
| .forEach((ClassInfo clazzInfo) -> { | ||
| var clazz = getClassFromClassInfo(clazzInfo); | ||
| var simpleName = clazzInfo.getSimpleName(); | ||
| alias(simpleName, clazz); | ||
| }); |
There was a problem hiding this comment.
Null return from getClassFromClassInfo is passed to alias() without check.
getClassFromClassInfo is annotated @Nullable and returns null on ClassNotFoundException. The lambda at line 501 passes this potentially-null value directly to alias(simpleName, clazz), which may cause unexpected behavior or NPE in XStream.
Suggested fix
.forEach((ClassInfo clazzInfo) -> {
var clazz = getClassFromClassInfo(clazzInfo);
+ if (clazz == null) {
+ return;
+ }
var simpleName = clazzInfo.getSimpleName();
alias(simpleName, clazz);
});,
📝 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.
| .forEach((ClassInfo clazzInfo) -> { | |
| var clazz = getClassFromClassInfo(clazzInfo); | |
| var simpleName = clazzInfo.getSimpleName(); | |
| alias(simpleName, clazz); | |
| }); | |
| .forEach((ClassInfo clazzInfo) -> { | |
| var clazz = getClassFromClassInfo(clazzInfo); | |
| if (clazz == null) { | |
| return; | |
| } | |
| var simpleName = clazzInfo.getSimpleName(); | |
| alias(simpleName, clazz); | |
| }); |
🤖 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
`@src/main/java/com/github/_1c_syntax/bsl/reader/common/xstream/ExtendXStream.java`
around lines 500 - 504, getClassFromClassInfo is `@Nullable` and its return
(clazz) is passed into alias(simpleName, clazz) without a null check; modify the
lambda that iterates over ClassInfo (the .forEach block using clazzInfo,
simpleName, clazz) to guard against null: call getClassFromClassInfo(clazzInfo),
and if it returns null skip calling alias(simpleName, clazz) (optionally log or
warn including clazzInfo.getName() or simpleName) so alias never receives a null
class and no NPE/undefined behavior occurs.
…e-блокеров При включённой переменной окружения BSL_LS_THREAD_DUMP бинарь через 60 секунд после старта печатает дамп стеков всех потоков и продолжает работу. Удобно для диагностики deadlock'ов в native-runtime, которые иначе не привязываются к gdb (ptrace_scope) и не реагируют на SIGQUIT в Substrate VM. Без переменной — поведение не меняется. Daemon-поток, не удерживает JVM. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Огромный шаг вперёд — нативная сборка теперь доходит до фактического анализа bsl-файлов и пишет SARIF. Последние коммиты по этому фронту:
Что работает сейчас:
Что не работает — runtime-deadlock на больших проектах: При
На JVM этого нет — Сравнение native vs JVM по производительности пока не получается провести по той же причине. Продолжу в следующих коммитах. |
…тает Закрыты оба root-cause'а runtime-зависания, обнаруженные при native-`analyze`. Это были два кооперирующихся бага в нашем коде, **не SVM** — на JVM они существуют, но не триггерятся. ## Root cause #1: Spring AOT не вызывает @Autowired-setter'ы для prototype-бинов `CyclomaticComplexityComputer` и `CognitiveComplexityComputer` (оба prototype, создаются через `ObjectProvider.getObject(documentContext)`) имели `@Setter(onMethod_={@Autowired}) StringInterner stringInterner` — тот же AOT-баг, который мы уже фиксили в `DocumentContext`: chained `andThen(__Autowiring::apply)` в native не отрабатывает. Поле `stringInterner` оставалось `null` → NPE на `stringInterner.intern(...)` внутри `addSecondaryLocation`. Фикс: setter сделан public, `DocumentContext` после `getObject(...)` вызывает `computer.setStringInterner(stringInterner)` вручную. Сам stringInterner приезжает через `ServerContext` (constructor inject) → передаётся в `DocumentContext.initializeDependencies(...)`. ## Root cause #2: Lazy.getOrCompute теряет lock на исключении В `io.github.1c-syntax:utils:0.7.0/com/github/_1c_syntax/utils/Lazy.java:60-68` {@code lock.unlock()} стоит после {@code maybeCompute(supplier)} вне try/finally: ```java public T getOrCompute(Supplier<T> supplier) { final T result = value; if (result == null) { lock.lock(); var localResult = maybeCompute(supplier); // exception → unlock пропущен lock.unlock(); return localResult; } return result; } ``` Любое исключение в `supplier.get()` оставляет lock захваченным навсегда. В `DocumentContext` несколько `Lazy<>` шарят общий `computeLock` (`contentList`, `moduleType`, `cognitiveComplexityData`, `cyclomaticComplexityData`, `diagnosticIgnoranceData`, `metrics`). После NPE в `computeCyclomaticComplexity` (root cause #1) lock остаётся занятым, все следующие обращения к `getAst`/`getSymbolTree` ([@locked]("computeLock") от Lombok) виснут. Лечится shadow-копией `Lazy` в нашем `src/main/java` с `lock.unlock()` в `finally` (классическая корректная реализация). Подменяется через стандартный classloader-precedence. На JVM баг тоже потенциально опасен, но в нашем рантайме там не было исключений в supplier'ах computeLock-ассоциированных Lazy → не триггерилось. ## Дополнительно: hints - `net/loomchild/segment/**` resource-globs (нужны LanguageTool для парсинга SRX); - 82 класса `com.contrastsecurity.sarif.*` с `allDeclaredFields`/`Constructors`/`Methods` — без них Jackson 3 сериализатор тихо отдавал пустой `{}` SARIF. ## Подтверждение и бенч `./build/native/nativeCompile/bsl-language-server analyze --srcDir ~/ssl_3_2/src/cf --reporter sarif --outputDir /tmp/out`: | | JVM (java -jar exec) | Native (executable) | |--------------------|----------------------|---------------------| | Время | 5m29s | **2m10s** | | Размер бинаря | 103 MB (exec.jar) | 140 MB | | Размер SARIF | 267 MB | 258 MB | | Холодный старт | ~3 c | ~0.2 c | Native в **~2.5x быстрее JVM** на анализе 2175 файлов 1С-конфигурации ssl_3_2 — и это без специальных оптимизаций образа (`-march=native`, `-O3`, профильная компиляция). Холодный старт CLI-команды (version/--help) на native ~200мс, на JVM ~3с — выигрыш на коротких вызовах ещё больше. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
🎉 Native-сборка работает end-to-end. Анализ ssl_3_2 в native ~2.5x быстрее JVM. Root cause runtime-зависания (как и было обещано, не из пальца)Это не Substrate VM, а два бага в нашем коде, которые на JVM не триггерятся: 1. Spring AOT не вызывает 2. public T getOrCompute(Supplier<T> supplier) {
final T result = value;
if (result == null) {
lock.lock();
var localResult = maybeCompute(supplier); // ← exception → unlock пропущен
lock.unlock(); // ← НЕ в finally
return localResult;
}
return result;
}Как соединяется: NPE из (1) пробрасывается через Что зафикшено в
|
JVM (java -jar exec) |
Native (executable) | |
|---|---|---|
| Время | 5m29s | 2m10s |
| Размер бинаря | 103 MB (exec.jar) |
140 MB |
| Размер SARIF | 267 MB | 258 MB |
| Холодный старт | ~3 c | ~200 мс |
| 167 результатов на 1 файле | да | да |
Native в ~2.5x быстрее JVM на полном проходе и ~15x быстрее на холодном старте — без -march=native/-O3/PGO. Холодный старт особенно ощущается на CLI-командах вроде version/--help.
Summary
Подключаю
org.graalvm.buildtools.nativeи довожу./gradlew nativeCompileдо успешной сборки бинаря размером ~125 MB. По пути правится несколько проблем, мешавших Spring AOT и native-image:AspectJBeanFactoryInitializationAotProcessorпарсит pointcut-выражения@Aspect-бинов черезReflectiveAspectJAdvisorFactory, которому нуженorg.aspectj.weaver.tools.PointcutParser. Раньше при запускеprocessAotполучалиClassNotFoundException: org.aspectj.weaver.reflect.ReflectionWorld$ReflectionWorldException. Лечится подкидываниемaspectjweaverтолько в classpathProcessAot/ProcessTestAot— рантайм/bootJarостаются «чистыми», CTW (io.freefair.aspectj.post-compile-weaving) продолжает работать как раньше.org.languagetool:languagetool-core(и языковые модули) транзитивно тянутio.grpc.netty-shaded. Его статические инициализаторы захватываютch.qos.logback.classic.Loggerв image heap, native-image валится. Используется он только для удалённых правил (n-gram сервис), у нас не задействован — выкидываетсяexclude-ами в трёх dependency-блоках LanguageTool.graalvmNative { binaries { named("main") { ... } } }: явно ставлюsharedLibrary.set(false), mainClass =BSLLSPLauncher,--no-fallback, отчёт стектрейсов иUnlockExperimentalVMOptions.@Component @Scope(value = WorkspaceScope.SCOPE_NAME, proxyMode = ScopedProxyMode.TARGET_CLASS)бина генерирует<FQN>$$SpringCGLIB$$1, но регистрирует ему толькоallDeclaredConstructors. Native-image затем падает на доступе к статическому полюCGLIB$FACTORY_DATA. Кладём дополнительныйMETA-INF/native-image/.../reachability-metadata.jsonс полной рефлексией на эти 22 класса (генерируется по@Scope(... TARGET_CLASS)вsrc/main/java).Thread.currentThread().getContextClassLoader().getResourceAsStream("META-INF/MANIFEST.MF")в native-image возвращаетnull,Manifest.read(null)падает с NPE. Добавлен null-check с fallback наPackage.getImplementationVersion().После этого
./gradlew nativeCompileсобирает executable (build/native/nativeCompile/bsl-language-server, ~125 MB, время сборки ~3.5 мин на 12-ядерной машине). Сборка проходит и под GraalVM CE 25.0.2.Что НЕ работает
aspectjweaverв runtime — Spring AOP активно создаёт CGLIB-прокси черезEnhancer.create()для бинов с@Cacheable/@Lazy self. На pre-generated$$SpringCGLIB$$0пытается выставить callbacks несовместимого типа →ClassCastException: CglibAopProxy$SerializableNoOp cannot be cast to DispatcherприDebugTestCodeLensSupplier.setCallbacks.aspectjweaverв runtime — Spring AOT-зарегистрированныеsentryCheckInAdvisor/cacheAdvisorпадают сNoClassDefFoundError: AspectJExpressionPointcut.Чтобы бинарь нормально стартовал, нужно:
@Lazy self-injection вDebugTestCodeLensSupplier/RunTestCodeLensSupplier/RunAllTestsCodeLensSupplier(заменить, например, наApplicationContextAware+ ленивый lookup),@Cacheableчерез прокси — кэширование нужно будет перевести на CTW-аспект или Caffeine напрямую),Это пока выносится за рамки PR — задача отдельной итерации.
Performance comparison (native vs
java -jar)Запланированное сравнение времени
analyzeнаssl_3_2с SARIF-репортером не получилось провести: native-бинарь падает на старте контекста (см. выше), поэтому до самой логики анализа дело не доходит. Сравнение будет иметь смысл после фикса runtime-блокеров.Test plan
./gradlew bootJarсобирает*-exec.jarкак раньше;java -jar ... --version→version: feature-update-deps-..../gradlew compileTestJava— без ошибок./gradlew :test --tests "*AutoServerInfo*"— проходит (testDataIsFilled() PASSED)./gradlew processAot— больше не падает наReflectionWorld$ReflectionWorldException./gradlew nativeCompileподGRAALVM_HOME=GraalVM CE 25.0.2—BUILD SUCCESSFUL, executable созданbsl-language-server— пока fail на инициализации Spring-контекста (см. «Что НЕ работает»)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
New Component
Bug Fixes
Refactor