Skip to content

feat(types): тип возвращаемого значения рассчитывается по телу метода - #4402

Merged
nixel2007 merged 21 commits into
developfrom
feat/types-return-types-from-body
Aug 8, 2026
Merged

feat(types): тип возвращаемого значения рассчитывается по телу метода#4402
nixel2007 merged 21 commits into
developfrom
feat/types-return-types-from-body

Conversation

@nixel2007

@nixel2007 nixel2007 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Пункт 2.26 методической рекомендации «Типизация кода»: тип возвращаемого значения функции рассчитывается по её телу, а объявленный в документирующем комментарии тип его дополняет, а не заменяет (пункт 2.11: «Типизирующие комментарии не могут переопределять типы, которые рассчитала EDT, а могут только их дополнять»).

Функция РазныеТипы(Флаг)
	Если Флаг Тогда
		Возврат Новый Массив;
	Иначе
		Возврат 10;
	КонецЕсли;
КонецФункции

Результат = РазныеТипы(Ложь);   // Массив, Число

Как считается

Точки выхода берутся из графа потока управления — в вершину выхода ведут и операторы Возврат, и достижимый конец тела:

последний оператор блока вклад
Возврат <выражение> типы выражения
ВызватьИсключение ничего: значение не возвращается
ничего из этого — блок дотёк до конца тела Неопределено, потому что функция без явного возврата возвращает именно его

Граф берётся из готового ControlFlowGraphIndex, отдельный обход дерева не нужен. У функции, где значение возвращается на всех путях, лишнего Неопределено не появляется.

По общим типам верим описанию: у Массив из Число из документирующего комментария состав элементов не размывается платформенным умолчанием от Новый Массив в теле.

Где считается

MethodReturnTypeIndex — workspace-scoped индекс, считающий значения в момент построения контекста документа, пока его дерево разбора под рукой. Читать дерево чужого документа нельзя: его вторичные данные могут быть освобождены ради памяти, и результат зависел бы от того, загружен ли документ прямо сейчас. Потребители берут из индекса готовое.

Рекурсии по стеку между методами нет: при расчёте тела значения вызванных методов читаются из индекса как есть, а до неподвижной точки их доводит пересчёт по зависимым. Индекс держит связи «метод → потребители»; изменилось значение — волна идёт по цепочке. Набор типов при пересчёте только растёт, поэтому взаимная рекурсия сходится; число проходов ограничено предохранителем.

Заранее считаются только экспортные функции — из чужих модулей видны лишь они. Неэкспортные считаются по запросу внутри своего документа и кэшируются там же.

Освобождение вторичных данных и закрытие документа записи не трогают: содержимое не менялось, дерево символов освобождение переживает, а значение и заводилось ради чтения без дерева разбора. Сбрасывают запись только правка и удаление файла.

Доразрешение после наполнения рабочей области

Документы разбираются параллельно и в произвольном порядке, поэтому вызов в модуль, ещё не зарегистрированный на тот момент, никуда не ведёт. На ServerContextPopulatedEvent откладыванные методы пересчитываются — с догрузкой документа, если его данные освобождены. Проход идёт в пуле рабочей области: его воркеры несут её контекст, без которого workspace-бины из форков потока недоступны.

Кэш выражений

InferredExpressionTypeIndex получил обратный индекс зависимостей: запись знает, на каких документах построена, и сбрасывается вместе с ними по цепочке (с защитой от круговых зависимостей). Результат, полученный с обрывом цикла, в кэш не пишется — он зависит от точки входа в цикл.

Замеры

analyze по ssl_3_1, без JFR, одна машина:

база (develop) с правкой
время 101 с 97 с
исключений в логе 0 0
замечаний 72 898 72 951 (+53)

Прирост замечаний — только там, где он и ожидается от лучшей типизации: DeprecatedMethodCall +29, CompareWithBoolean +22, AssignToReadOnlyProperty +2. Потерь нет.

Замер с JFR и прогон на cpm — в работе, выложу в комментарии.

Тесты

ReturnTypeFromBodyInferenceTest — восемь случаев: объединение веток, дополнение объявленным типом, Неопределено на достижимом конце тела, отсутствие лишнего Неопределено когда все пути возвращают, ВызватьИсключение, самоссылка, процедура, чтение тела метода другого модуля. Плюс два теста на обратный индекс зависимостей кэша выражений.

Ожидания трёх тестов InlineTypeCommentInferenceTest приведены к правилу «комментарий дополняет»: заглушка Функция ВызовФункции() Возврат "stub" теперь честно даёт Строка вдобавок к типу из комментария.

🤖 Generated with Claude Code

https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc

Summary by CodeRabbit

  • New Features

    • Return types can now be inferred from function code, including branches and reachable fallthrough paths.
    • Hover information and signature help include inferred return types when documentation is incomplete.
    • Hover details indicate when inferred types differ from documented return values.
    • Type information refreshes across dependent documents when source code changes.
    • Return-type resolution progress is now reported during workspace analysis.
  • Bug Fixes

    • Improved handling of recursive and circular references.
    • Prevented stale inferred type information after documents are cleared or closed.
    • Combined documented and inferred return types without duplicating entries.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@nixel2007, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 7 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8f776c36-1ff9-45ed-a80b-6437db8b1f82

📥 Commits

Reviewing files that changed from the base of the PR and between b1005c7 and ec6b60a.

📒 Files selected for processing (2)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/TypeRegistry.java
📝 Walkthrough

Walkthrough

The change adds body-based method return inference, dependency-aware indexing and propagation, inferred return types in hover and signature help, document-state access, generic-prefix lookup caching, and Python command permission rules.

Changes

Method return-type inference

Layer / File(s) Summary
Document state contract
src/main/java/.../context/*
Adds public document-content states and exposes document state through ServerContext.
Body inference and type storage
src/main/java/.../types/inferencer/*, src/main/java/.../types/index/SymbolTypeIndex.java, src/main/java/.../types/TypeService.java
Infers return types from control-flow exits, combines them with documented types, and handles recursion and cross-document references.
Dependency-aware indexing
src/main/java/.../types/index/*, src/main/resources/.../MethodReturnTypeIndexer_*.properties
Tracks document dependencies, invalidates transitive caches, propagates changed return types, resolves cycles, and reports progress.
Language-server consumers
src/main/java/.../hover/*, src/main/java/.../providers/SignatureHelpProvider.java, src/main/resources/.../DescriptionFormatter_*.properties
Uses inferred return types in signature help and hover content, including localized divergence notes.
Validation
src/test/java/.../types/*, src/test/java/.../hover/*, src/test/java/.../architecture/ArchitectureTest.java
Tests body inference, recursion, dependency invalidation, propagation, combined types, and hover output.

Generic prefix lookup cache

Layer / File(s) Summary
Alias lookup caching
src/main/java/.../types/registry/TypeRegistry.java
Caches generic-prefix lookups, including empty results, and clears the cache when aliases change.

Command permission rules

Layer / File(s) Summary
Python command denial
.claude/settings.json
Denies direct and piped python and python3 Bash commands.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ServerContext
  participant MethodReturnTypeIndexer
  participant ExpressionTypeInferencer
  participant SymbolTypeIndex
  ServerContext->>MethodReturnTypeIndexer: document content change
  MethodReturnTypeIndexer->>ExpressionTypeInferencer: recompute method return types
  ExpressionTypeInferencer->>SymbolTypeIndex: store inferred types
  MethodReturnTypeIndexer->>MethodReturnTypeIndexer: invalidate and recompute dependent documents
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.42% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: calculating method return types from method bodies.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/types-return-types-from-body

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Test Results

 4 038 files  + 18   4 038 suites  +18   58m 57s ⏱️ + 13m 53s
 4 188 tests + 24   4 117 ✅ + 24   71 💤 ±0  0 ❌ ±0 
25 128 runs  +144  24 698 ✅ +144  430 💤 ±0  0 ❌ ±0 

Results for commit ec6b60a. ± Comparison against base commit efa5187.

This pull request removes 1 and adds 25 tests. Note that renamed tests count towards both.
com.github._1c_syntax.bsl.languageserver.types.TypeServiceDelegationTest ‑ getDeclaredReturnTypesDelegatesToSymbolTypeIndex()
com.github._1c_syntax.bsl.languageserver.hover.MethodSymbolMarkupContentBuilderTest ‑ testDescribedReturnedValueIsShownWithDivergence()
com.github._1c_syntax.bsl.languageserver.hover.MethodSymbolMarkupContentBuilderTest ‑ testDescribedReturnedValueWithoutDivergenceHasNoNote()
com.github._1c_syntax.bsl.languageserver.hover.MethodSymbolMarkupContentBuilderTest ‑ testReturnedValueSectionIsInferredWhenNotDescribed()
com.github._1c_syntax.bsl.languageserver.types.ReturnTypeFromBodyInferenceTest ‑ bodyOfAnotherModuleMethodIsRead()
com.github._1c_syntax.bsl.languageserver.types.ReturnTypeFromBodyInferenceTest ‑ branchesGiveUnionOfReturnedTypes()
com.github._1c_syntax.bsl.languageserver.types.ReturnTypeFromBodyInferenceTest ‑ declaredTypeExtendsComputedOnes()
com.github._1c_syntax.bsl.languageserver.types.ReturnTypeFromBodyInferenceTest ‑ everyPathReturningValueDoesNotAddUndefined()
com.github._1c_syntax.bsl.languageserver.types.ReturnTypeFromBodyInferenceTest ‑ procedureHasNoReturnedValue()
com.github._1c_syntax.bsl.languageserver.types.ReturnTypeFromBodyInferenceTest ‑ raiseIsNotAReturnedValue()
com.github._1c_syntax.bsl.languageserver.types.ReturnTypeFromBodyInferenceTest ‑ reachableBodyEndAddsUndefined()
…

♻️ This comment has been updated with latest results.

@sfaqer

sfaqer commented Aug 5, 2026

Copy link
Copy Markdown
Member

Смотрю со стороны параллельной ветки — в #4406 задет тот же кусок ExpressionTypeInferencer, поэтому на всякий случай отмечу, пока работа не закончена.

Кэш типов выражений: два разных «неокончательных ответа»

Здесь:

if (cacheKey != null && !ctx.cycleCut) {
  inferredExpressionTypeIndex.put(uri, cacheKey, result, ctx.dependencies);

В #4406 правится соседняя строка — вычисление cacheKey:

var cacheKey = ctx.visited.isEmpty() && ctx.inProgress.isEmpty()
  && !ctx.flowSession.computing() ? node.getRepresentingAst() : null;

Причина у обоих одна: в кэш попадал ответ, который ещё не окончателен. Но условия разные и друг друга не закрывают:

  • cycleCut — результат усечён защитой от рекурсии по символам;
  • flowSession.computing() — расчёт по потоку ещё не дошёл до неподвижной точки. Правая часть каждого присваивания вычисляется изнутри этого расчёта, поэтому в кэш оседало приближение и дальше подменяло собой посчитанный ответ. Наружу это вылезало так: при вложенном условии про ту же переменную слияние за внешним КонецЕсли теряло целую ветку, хотя окружение перед оператором расчёт считал верно.

Так что после обоих мержей условие должно стать составным, а не «чьим-то одним». Второму по очереди достанется ещё и адаптация под новую сигнатуру InferredExpressionTypeIndex.put — она тут меняется.

Про замеры

Мы этот же вывод возврата по телу пробовали лениво — считать изнутри инференса, с кэшем по методу, — и уткнулись: полный analyze ERP (25 126 файлов, UnknownMember включён) вырос с 1494 с до 8400+ с, то есть в 5,6 раза, при 54 505 CPU-с. Кэш почти не срабатывал, потому что расчёт заходил изнутри потока. Из-за этого работу и отложили; предвычисленный индекс, как здесь, был записан как единственный годный вариант — так что подход выглядит верным.

Если полного прогона ещё не было, цифра выше годится как база для сравнения. Гочки, которые нам дорого дались: -w и -o должны лежать на том же диске, что исходники (иначе IllegalArgumentException: 'other' has different root уже на анализе), POSIX-пути JVM не понимает, и весь вывод в файл перенаправлять нельзя — при потоке ошибок лог вырастает до десятков гигабайт и сам искажает время.

🤖 Generated with Claude Code

@nixel2007
nixel2007 marked this pull request as ready for review August 7, 2026 15:59
@nixel2007

Copy link
Copy Markdown
Member Author

Спасибо, оба пункта по делу.

Про условие кэша

Согласен, условия разные и друг друга не закрывают. На этой ветке ctx.flowSession есть, а
метода computing() пока нет — он появляется у вас в #4406. Поэтому составное условие ляжет
на того, кто сливается вторым:

// было
void put(URI uri, ParseTree node, TypeSet types);
// стало
void put(URI uri, ParseTree node, TypeSet types, Set<URI> dependencies);

Четвёртый параметр — URI документов, чьё содержимое участвовало в расчёте; по ним индекс
сбрасывает зависимые записи при правке чужого модуля.

Про замеры

Полного прогона по ERP у меня не было — мерил на другой конфигурации (23 575 модулей,
Typo выключен, остальное включено). Числа на одинаковых условиях, куча 16 ГБ:

база ветка
время анализа 1382 с 1108 с

То есть ветка вышла быстрее базы. Ключевым оказался не сам вывод возврата, а то, что он
сделал горячим TypeRegistry.resolveGenericByPrefix — линейный перебор индекса имён на
каждое разрешаемое имя. После мемоизации (отдельный коммит в этой же ветке) он ушёл из
профиля совсем: у базы верхние строки — String.startsWith 78 743 и ArraysSupport.mismatch
53 696 сэмплов, у ветки их в топе нет.

Ваш вывод про ленивый расчёт подтверждаю с другой стороны: проход доразрешения после
наполнения рабочей области сходится за одну волну и разбирает каждый документ ровно один раз
(на этой конфигурации 28 разборов на 28 документов). До упорядочивания по компонентам
сильной связности тех же документов разбиралось 215–301 раз.

По диагностикам: ветка добавляет 100 363 срабатывания UnknownMember — целиком за счёт того,
что тип получателя теперь выводится там, где раньше был неизвестен и проверка молчала.
Остальные шестнадцать диагностик совпадают с базой до единицы.

За гочки по запуску спасибо — учту, если буду гонять ERP.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

🧹 Nitpick comments (8)
.claude/settings.json (1)

11-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant pipe-based Bash rules.

Claude Code evaluates compound Bash commands as segments, so Bash(python:*) and Bash(python3:*) already cover the interpreter segment in commands such as command | python. These added Bash(*|python:*)/Bash(*|python3:*) rules do not add coverage and depend on unverified parser behavior.

Proposed cleanup
       "Bash(python:*)",
-      "Bash(python3:*)",
-      "Bash(*|python:*)",
-      "Bash(*|python3:*)"
+      "Bash(python3:*)"
🤖 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 @.claude/settings.json around lines 11 - 12, Remove the redundant
Bash(*|python:*) and Bash(*|python3:*) entries from the permissions
configuration, leaving the existing Bash(python:*) and Bash(python3:*) rules
unchanged.
src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java (1)

242-244: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Join assertions on the same value.

SonarCloud reports duplicate assertion subjects at Lines 242 and 263. Chain the contains calls to keep each scenario as one assertion chain.

Proposed cleanup
-    assertThat(content).contains("**Возвращаемое значение:**");
-    assertThat(content).contains("Массив");
-    assertThat(content).contains("Число");
+    assertThat(content)
+      .contains("**Возвращаемое значение:**")
+      .contains("Массив")
+      .contains("Число");

Also applies to: 263-265

🤖 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/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java`
around lines 242 - 244, Combine the consecutive contains assertions for the same
content value in MethodSymbolMarkupContentBuilderTest into a single fluent
assertion chain for each scenario, including both the assertions around lines
242 and 263, while preserving all existing expected substrings.

Source: Linters/SAST tools

src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/DocumentDependencies.java (1)

24-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused MethodSymbol import.

The file does not reference MethodSymbol. Static analysis reports it as unused.

♻️ Proposed fix
-import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol;
-
 import java.net.URI;
🤖 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/types/index/DocumentDependencies.java`
at line 24, Remove the unused MethodSymbol import from
DocumentDependencies.java, leaving the remaining imports and implementation
unchanged.

Source: Linters/SAST tools

src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/MethodReturnTypeIndexer.java (1)

465-500: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce the cognitive complexity of resolveComponent.

SonarCloud reports a failure: cognitive complexity 18 against the allowed 15. The method mixes three strategies (single document, oversized component, in-memory component) and repeats the same fixed-point loop twice. Extract the two loop bodies into named private methods, for example resolveLargeComponent and resolveLoadedComponent. The behaviour stays the same and each strategy becomes readable on its own.

🤖 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/types/index/MethodReturnTypeIndexer.java`
around lines 465 - 500, Reduce cognitive complexity in resolveComponent by
extracting the oversized-component pass loop into a private
resolveLargeComponent method and the withDocumentsLoaded fixed-point loop into a
private resolveLoadedComponent method. Keep the existing single-document
handling, MAX_PASSES termination, changed checks, and return behavior unchanged
while delegating each strategy from resolveComponent.

Source: Linters/SAST tools

src/test/java/com/github/_1c_syntax/bsl/languageserver/types/MapElementFieldsInferenceTest.java (1)

50-52: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Select the map reference by name instead of by position.

getReturnTypes can now return several references: the declared type plus the types inferred from the body. Line 51 takes the first reference. The assertion passes only because TypeSet.union keeps the declared references first. The same test file family already moved to explicit selection for this reason — see InlineTypeCommentInferenceTest lines 86-89. Selecting Соответствие by qualified name removes the ordering dependency. The variable name declared is also stale now that the value includes inferred types.

♻️ Proposed change
-    var declared = typeService.getReturnTypes(method("Тело"));
-    var mapRef = declared.refs().iterator().next();
-    var element = declared.getElementTypes(mapRef);
+    var returned = typeService.getReturnTypes(method("Тело"));
+    var mapRef = returned.refs().stream()
+      .filter(ref -> "Соответствие".equals(ref.qualifiedName()))
+      .findFirst()
+      .orElseThrow();
+    var element = returned.getElementTypes(mapRef);
🤖 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/test/java/com/github/_1c_syntax/bsl/languageserver/types/MapElementFieldsInferenceTest.java`
around lines 50 - 52, Update the test around getReturnTypes in
MapElementFieldsInferenceTest to select the Соответствие map reference by its
qualified name instead of taking declared.refs().iterator().next(). Rename the
stale declared variable to reflect that it contains inferred and declared types,
while preserving the existing element-type assertion.
src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java (2)

802-808: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log the failure before returning an empty result.

returnTypesOfBody catches StackOverflowError and RuntimeException and returns an empty ComputedReturnTypes with incomplete=false. The indexer stores that value as a final answer, so a crash in body inference becomes a silent "this function returns nothing" for the whole workspace. The surrounding code logs comparable failures, for example flowTypeAt at lines 850-855. SonarCloud also flags line 805.

♻️ Proposed change
     } catch (StackOverflowError | RuntimeException e) {
+      LOGGER.error("Расчёт типа возврата по телу сорвался на методе {}: {}",
+        method.getName(), method.getOwner().getUri(), e);
       return new MethodReturnTypeIndexer.ComputedReturnTypes(TypeSet.EMPTY, Set.of(), false);
     }
🤖 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/types/inferencer/ExpressionTypeInferencer.java`
around lines 802 - 808, Update returnTypesOfBody to log the caught
StackOverflowError or RuntimeException before returning the empty
ComputedReturnTypes result. Follow the existing failure-logging pattern used by
flowTypeAt, preserving the current fallback return value while including the
exception details.

Source: Linters/SAST tools


643-653: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the source-method branch to clear the SonarCloud gate.

SonarCloud reports a failure on line 649: more than three nested if/for statements. The new symbolReturn block adds the fourth level inside the for/for/if chain. Move the member handling into a small private method that returns the resolved TypeSet for one member.

🤖 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/types/inferencer/ExpressionTypeInferencer.java`
around lines 643 - 653, Extract the source-method handling from the member
iteration in ExpressionTypeInferencer into a small private helper that resolves
and returns the TypeSet for a single member, including the MethodSymbol
return-type lookup and fallback behavior. Update the existing loop to call this
helper and union its result, reducing nesting without changing inference
semantics.

Source: Linters/SAST tools

src/test/java/com/github/_1c_syntax/bsl/languageserver/types/ReturnTypeFromBodyInferenceTest.java (1)

235-243: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Share one marker-to-position helper across the type tests.

This helper duplicates InlineTypeCommentInferenceTest.inferAtMarker and MapElementFieldsInferenceTest.at, and it uses a different line-start rule: lastIndexOf('\n', targetOffset - 1) here versus lastIndexOf('\n', targetOffset) in the other two. The two rules disagree when the offset lands on a newline. Move one implementation into TestUtils and call it from all three tests.

🤖 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/test/java/com/github/_1c_syntax/bsl/languageserver/types/ReturnTypeFromBodyInferenceTest.java`
around lines 235 - 243, Move the marker-to-Position logic from
ReturnTypeFromBodyInferenceTest.at,
InlineTypeCommentInferenceTest.inferAtMarker, and
MapElementFieldsInferenceTest.at into a shared TestUtils helper, preserving
marker validation and a single consistent newline boundary rule. Replace all
three local helpers with calls to TestUtils and update callers as needed.
🤖 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/languageserver/hover/DescriptionFormatter.java`:
- Around line 171-181: Split each documented returned-value name on commas
before applying DescriptionFormatter.headName, then normalize and compare the
resulting individual type names against returnTypes.refs() so documented union
members are not classified as undescribed. Apply the same correction to the
analogous logic around the second reported range, and add a hover test covering
documented “Число, Строка” with matching branch returns that asserts
inferredReturnedValue is absent.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java`:
- Around line 494-495: Update SignatureInfoProvider.methodToDescriptor(...) to
preserve all alternatives from typeService.getReturnTypes(method) instead of
selecting one with findFirst(). Build the return label from the complete
returnTypes.refs() set, or suppress the specific type when multiple alternatives
cannot be represented, while retaining TypeRef.UNKNOWN for an empty result.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/MethodReturnTypeIndexer.java`:
- Around line 510-535: Update withDocumentsLoaded to acquire document write
locks in a consistent global order by sorting the component URIs before
iterating and locking them. Preserve the existing document lookup, rebuild, work
execution, cleanup, and reverse-order unlock behavior.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/SymbolTypeIndex.java`:
- Around line 171-179: Update SymbolTypeIndex.putReturnTypes and the
inferredByUri map to use a set per URI instead of a CopyOnWriteArrayList,
ensuring repeated recomputation does not retain duplicate MethodSymbol
references. Remove the method from the URI set when types is empty, and review
clear(uri) ordering or synchronization so concurrent putReturnTypes calls cannot
leave inferredReturnTypes entries unreachable from a later clear.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java`:
- Around line 795-808: Update returnTypesOfBody to isolate ctx.consulted and
ctx.sawMissing for each method-body computation by using fresh per-call tracking
state, then merge that state into the parent context after inference completes,
including exceptional paths as appropriate. Ensure ComputedReturnTypes contains
only dependencies and incompleteness produced by the current method. When
ctx.depth reaches MAX_DEPTH, return the empty result with incomplete=true so
truncated inference is reprocessed consistently with cycleCut.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ReturnTypeFromBodyInference.java`:
- Around line 105-117: Update typesOfExit in ReturnTypeFromBodyInference to
store the result of
ExpressionTreeBuildingVisitor.buildExpressionTree(expression), check it for
null, and call expressionTypes.of only for a non-null tree; return the existing
empty TypeSet fallback otherwise. Preserve the current handling for absent
return expressions, raise statements, and undefined results.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/TypeRegistry.java`:
- Around line 370-375: Synchronize generic-prefix cache lookups and alias
mutations so stale results cannot be republished after invalidation. Update the
lookup containing genericByPrefix.computeIfAbsent and the alias mutation/removal
paths, including the operation near the alias removal site, to use one shared
lock or equivalent epoch-checked publication; route all direct alias writes
through the same protocol. Add concurrency coverage for lookups racing alias
addition and removal.

---

Nitpick comments:
In @.claude/settings.json:
- Around line 11-12: Remove the redundant Bash(*|python:*) and Bash(*|python3:*)
entries from the permissions configuration, leaving the existing Bash(python:*)
and Bash(python3:*) rules unchanged.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/DocumentDependencies.java`:
- Line 24: Remove the unused MethodSymbol import from DocumentDependencies.java,
leaving the remaining imports and implementation unchanged.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/MethodReturnTypeIndexer.java`:
- Around line 465-500: Reduce cognitive complexity in resolveComponent by
extracting the oversized-component pass loop into a private
resolveLargeComponent method and the withDocumentsLoaded fixed-point loop into a
private resolveLoadedComponent method. Keep the existing single-document
handling, MAX_PASSES termination, changed checks, and return behavior unchanged
while delegating each strategy from resolveComponent.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java`:
- Around line 802-808: Update returnTypesOfBody to log the caught
StackOverflowError or RuntimeException before returning the empty
ComputedReturnTypes result. Follow the existing failure-logging pattern used by
flowTypeAt, preserving the current fallback return value while including the
exception details.
- Around line 643-653: Extract the source-method handling from the member
iteration in ExpressionTypeInferencer into a small private helper that resolves
and returns the TypeSet for a single member, including the MethodSymbol
return-type lookup and fallback behavior. Update the existing loop to call this
helper and union its result, reducing nesting without changing inference
semantics.

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java`:
- Around line 242-244: Combine the consecutive contains assertions for the same
content value in MethodSymbolMarkupContentBuilderTest into a single fluent
assertion chain for each scenario, including both the assertions around lines
242 and 263, while preserving all existing expected substrings.

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/MapElementFieldsInferenceTest.java`:
- Around line 50-52: Update the test around getReturnTypes in
MapElementFieldsInferenceTest to select the Соответствие map reference by its
qualified name instead of taking declared.refs().iterator().next(). Rename the
stale declared variable to reflect that it contains inferred and declared types,
while preserving the existing element-type assertion.

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/ReturnTypeFromBodyInferenceTest.java`:
- Around line 235-243: Move the marker-to-Position logic from
ReturnTypeFromBodyInferenceTest.at,
InlineTypeCommentInferenceTest.inferAtMarker, and
MapElementFieldsInferenceTest.at into a shared TestUtils helper, preserving
marker validation and a single consistent newline boundary rule. Replace all
three local helpers with calls to TestUtils and update callers as needed.
🪄 Autofix

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 Plus

Run ID: cc4b88c5-538c-4b5a-aa0f-6161d20d0334

📥 Commits

Reviewing files that changed from the base of the PR and between 0b45e3b and e1d0999.

📒 Files selected for processing (27)
  • .claude/settings.json
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/context/DocumentState.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/context/ServerContext.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/DescriptionFormatter.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilder.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/TypeService.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/DocumentDependencies.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/InferredExpressionTypeIndex.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/MethodReturnTypeIndexer.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/SymbolTypeIndex.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ReturnTypeFromBodyInference.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/registry/TypeRegistry.java
  • src/main/resources/com/github/_1c_syntax/bsl/languageserver/hover/DescriptionFormatter_en.properties
  • src/main/resources/com/github/_1c_syntax/bsl/languageserver/hover/DescriptionFormatter_ru.properties
  • src/main/resources/com/github/_1c_syntax/bsl/languageserver/types/index/MethodReturnTypeIndexer_en.properties
  • src/main/resources/com/github/_1c_syntax/bsl/languageserver/types/index/MethodReturnTypeIndexer_ru.properties
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/architecture/ArchitectureTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/hover/MethodSymbolMarkupContentBuilderTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/InlineTypeCommentInferenceTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/MapElementFieldsInferenceTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/NestedSeeRefInferenceTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/ReturnTypeFromBodyInferenceTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/TypeServiceDelegationTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/InferredExpressionTypeIndexTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/MethodReturnTypeIndexerTest.java

Comment on lines +494 to +495
var returnTypes = typeService.getReturnTypes(method);
var returnRef = returnTypes.refs().stream().findFirst().orElse(TypeRef.UNKNOWN);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the return-type representation used by signature help.
ast-grep outline src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java \
  --match methodToDescriptor --view expanded

rg -n -C 5 'record SignatureDescriptor|class SignatureDescriptor|returnType' src/main/java
rg -n -C 8 'getSignatureHelp|РазныеТипы|Возврат Новый Массив|Возврат 10' src/test/java

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

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- location candidates ---\n'
git ls-files | rg 'SignatureHelpProvider\.java|SignatureDescriptor|TypeRef|TypeSet|MethodReturnType|getReturnTypes'

printf '\n--- SignatureHelpProvider relevant area ---\n'
wc -l src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java
sed -n '460,520p' src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java

printf '\n--- SignatureDescriptor files ---\n'
for f in $(git ls-files | rg 'SignatureDescriptor\.java'); do
  echo "### $f"
  wc -l "$f"
  sed -n '1,220p' "$f"
done

printf '\n--- getReturnTypes definitions/usages non-test ---\n'
rg -n -C 5 'MethodReturnType|getReturnTypes|TypeSet\(null|TypeSet\.EMPTY|unknown\s*=\s*null|refs\(\)' src/main/java/com/github/_1c_syntax/bsl/languageserver | sed -n '1,260p'

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

Length of output: 258


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- location candidates ---'
git ls-files | rg 'SignatureHelpProvider\.java|SignatureDescriptor|TypeRef|TypeSet|MethodReturnType|getReturnTypes' || true

printf '%s\n' '--- SignatureHelpProvider relevant area ---'
wc -l src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java
sed -n '460,520p' src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java

printf '%s\n' '--- SignatureDescriptor files ---'
while IFS= read -r f; do
  echo "### $f"
  wc -l "$f"
  sed -n '1,220p' "$f"
done < <(git ls-files | rg 'SignatureDescriptor\.java')

printf '%s\n' '--- getReturnTypes definitions/usages non-test ---'
rg -n -C 5 'MethodReturnType|getReturnTypes|TypeSet\(null|TypeSet\.EMPTY|unknown\s*=\s*null|refs\(\)' src/main/java/com/github/_1c_syntax/bsl/languageserver | sed -n '1,260p' || true

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

Length of output: 4485


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- SignatureDescriptor files ---'
git ls-files | rg 'SignatureDescriptor\.java' | while IFS= read -r f; do
  echo "### $f"
  wc -l "$f"
  sed -n '1,220p' "$f"
done

printf '%s\n' '--- getReturnTypes definitions/usages non-test ---'
rg -n -C 5 'MethodReturnType|getReturnTypes|TypeSet\(null|TypeSet\.EMPTY|unknown\s*=\s*null|refs\(\)' src/main/java/com/github/_1c_syntax/bsl/languageserver | sed -n '1,320p' || true

printf '%s\n' '--- SignatureHelpProvider tests ---'
git ls-files | rg 'SignatureHelpProvider.*Test\.java' | while IFS= read -r f; do
  echo "### $f"
  rg -n -C 4 'return|Массив|Число|Возврат|Разные' "$f" || true
done

printf '%s\n' '--- TypeSet relevant APIs ---'
for f in $(git ls-files 'src/main/java/**/TypeSet.java' 'src/main/java/**/MethodReturnTypeIndexer.java'); do
  echo "### $f"
  wc -l "$f"
  sed -n '1,220p' "$f"
done

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

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- SignatureHelpProvider formatting method ---'
sed -n '600,645p' src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java

printf '%s\n' '--- TypeSet relevant APIs ---'
for f in $(git ls-files 'src/main/java/**/TypeSet.java' 'src/main/java/**/MethodReturnTypeIndexer.java'); do
  echo "### $f"
  wc -l "$f"
  sed -n '1,220p' "$f"
done

printf '%s\n' '--- focused signature-help union-related tests ---'
rg -n -C 5 'Массив|Число|Разн|Возврат|Скелет|Union|union|returnType|returnTypes' src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProviderTest.java || true

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

Length of output: 26195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- SignatureHelpProvider relevant full section ---'
sed -n '1,90p' src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java
sed -n '460,550p' src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProvider.java

printf '%s\n' '--- focused tests around function return signature ---'
sed -n '520,570p' src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SignatureHelpProviderTest.java

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

Length of output: 10465


Show all return alternatives or suppress the return type.

SignatureDescriptor already stores return types as TypeSet, but SignatureInfoProvider.methodToDescriptor(...) keeps only findFirst() and labels the signature with that single TypeRef. A function with multiple reachable return types, such as Массив in one branch and Число in another, shows only one return type. Build the return type label from all returnTypes.refs() or omit the specific return type for multi-alternative unions.

🤖 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/providers/SignatureHelpProvider.java`
around lines 494 - 495, Update SignatureInfoProvider.methodToDescriptor(...) to
preserve all alternatives from typeService.getReturnTypes(method) instead of
selecting one with findFirst(). Build the return label from the complete
returnTypes.refs() set, or suppress the specific type when multiple alternatives
cannot be represented, while retaining TypeRef.UNKNOWN for an empty result.

Comment on lines +795 to +808
private MethodReturnTypeIndexer.ComputedReturnTypes returnTypesOfBody(
MethodSymbol method,
InferenceContext ctx
) {
if (ctx.depth >= MAX_DEPTH) {
return new MethodReturnTypeIndexer.ComputedReturnTypes(TypeSet.EMPTY, Set.of(), false);
}
try {
var types = returnTypeFromBodyInference.of(method, expression -> inferInternal(expression, ctx));
return new MethodReturnTypeIndexer.ComputedReturnTypes(types, Set.copyOf(ctx.consulted), ctx.sawMissing);
} catch (StackOverflowError | RuntimeException e) {
return new MethodReturnTypeIndexer.ComputedReturnTypes(TypeSet.EMPTY, Set.of(), false);
}
}

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.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

ctx.consulted and ctx.sawMissing are shared by the whole inference, so a nested computation records dependencies that are not its own.

returnTypesOfBody snapshots Set.copyOf(ctx.consulted) and reads ctx.sawMissing. Both live on the shared InferenceContext. Line 754 calls returnTypesOfBody(method, ctx) inside an inference that already consulted other methods, so the snapshot for that nested method contains every method consulted earlier in the same context, plus the sawMissing flag raised by an unrelated branch.

MethodReturnTypeIndexer.store uses consulted() to build the dependency graph and incomplete() to schedule a re-pass. The extra edges make unrelated methods recompute whenever any of those documents change, and the sticky incomplete flag adds methods to pending that are already complete. Results stay correct, but the propagation set grows.

Record the consulted set and the missing flag per computation instead of per context. For example, swap in a fresh set for the duration of returnTypesOfBody and merge it back into the parent afterwards.

Line 799 has a related fidelity gap: when the depth limit stops the computation, the returned ComputedReturnTypes reports incomplete=false. The indexer then treats a truncated result as final. Consider reporting true there, as cycleCut already does for the expression cache.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 805-805: Either log or rethrow this exception.

See more on https://sonarcloud.io/project/issues?id=1c-syntax_bsl-language-server&issues=AZ_LuvkKVW2KpAAN8HH9&open=AZ_LuvkKVW2KpAAN8HH9&pullRequest=4402

🤖 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/types/inferencer/ExpressionTypeInferencer.java`
around lines 795 - 808, Update returnTypesOfBody to isolate ctx.consulted and
ctx.sawMissing for each method-body computation by using fresh per-call tracking
state, then merge that state into the parent context after inference completes,
including exceptional paths as appropriate. Ensure ComputedReturnTypes contains
only dependencies and incompleteness produced by the current method. When
ctx.depth reaches MAX_DEPTH, return the empty result with incomplete=true so
truncated inference is reprocessed consistently with cycleCut.

@nixel2007

Copy link
Copy Markdown
Member Author

@coderabbitai разобрал все семь. Пять исправил, два оставил — с обоснованием.

Исправлено (b1005c7)

Объединение типов в описании (DescriptionFormatter). Подтверждается: headName("Число, Строка")
возвращал число, — с запятой, — и совпадения не находилось, поэтому в ховере появлялась
приписка «выведено по коду» там, где автор всё описал. Имя из описания теперь разбивается по
запятой перед сравнением.

Дерево выражения возврата (ReturnTypeFromBodyInference). Проверка на null добавлена:
на нераспознанном тексте дерево не строится, а контракт вызываемого предполагает непустой узел.

Порядок взятия замков (MethodReturnTypeIndexer.withDocumentsLoaded). Замки берутся в
порядке имён документов: их держится сразу несколько, и без общего порядка два потока на
пересекающихся компонентах встали бы друг против друга.

Накопление повторов (SymbolTypeIndex.inferredByUri). Заменено на набор — но обоснование
уточню, потому что в исходной формулировке оно неточное: устаревшие символы тут ни при чём,
их снимает clear(uri), который стирает запись документа целиком. Настоящая цена в другом:
за одно поколение значение метода пересчитывается многократно — при разборе, в проходе
доразрешения, при разносе по потребителям, — и CopyOnWriteArrayList копировал весь массив
на каждое добавление.

Гонка сброса и записи (там же). Окно настоящее: clear снимал набор, затем обходил его, а
расчёт по запросу для неэкспортного метода идёт вне событий жизненного цикла и мог вклиниться
между этими шагами — значение оставалось в карте типов без ссылки из карты по документам,
недостижимым для следующего сброса. Обе операции переведены на compute по ключу документа,
то есть выполняются под одним замком.

Оставлено

Подсказка сигнатуры показывает только первый тип из объединения. Замечание верное, но
место не наше: SignatureHelpProvider эта ветка не трогает, ограничение существовало и
раньше. Ветка лишь сделала объединения обычным делом. Завёл отдельно: #4427.

Общий контекст вывода (ctx.consulted, ctx.sawMissing). Тоже верно: вложенный расчёт
снимает снимок, куда попали методы, консультированные не им, — граф зависимостей выходит шире
нужного, а флаг неполноты бывает липким. Результаты при этом правильные, страдает только
объём разноса, и на практике он мал: замер на конфигурации из 23 575 модулей показывает, что
проход доразрешения сходится за одну волну и разбирает каждый документ ровно один раз (28
разборов на 28 документов). Правка требует развести области видимости внутри контекста вывода —
это отдельная работа, а не довесок к этому PR.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@nixel2007 Спасибо за подробный разбор.

Принял исправления из b1005c78d5. Уточнение о причине замены CopyOnWriteArrayList на набор корректно. Исправление синхронизации clear и записи по ключу документа также закрывает описанное окно гонки.

Ограничение SignatureHelpProvider вынесено в #4427, поэтому не блокирует этот PR.

Замечание о совместных ctx.consulted и ctx.sawMissing остаётся актуальным, но не влияет на корректность типов. При измеренной сходимости за одну волну его можно вынести из объёма этого PR. Хотите, чтобы я создал отдельную GitHub issue для разделения областей видимости контекста вывода?

🐇


✏️ Learnings added
Learnt from: nixel2007
URL: https://github.com/1c-syntax/bsl-language-server/pull/4402

Timestamp: 2026-08-07T17:52:20.271Z
Learning: In the Java type-inference implementation, `SymbolTypeIndex.inferredByUri` can receive repeated method symbols during one document generation because return types are recomputed during parsing, deferred resolution, and propagation. Use a set rather than `CopyOnWriteArrayList` to avoid copying the whole collection on each duplicate insertion. `clear(URI)` already removes the complete document entry, so stale symbols are not the reason for this change.

Learnt from: nixel2007
URL: https://github.com/1c-syntax/bsl-language-server/pull/4402

Timestamp: 2026-08-07T17:52:20.271Z
Learning: In `MethodReturnTypeIndexer`, return-type calculation for a non-exported method can run outside document lifecycle events. Synchronize both clearing and insertion of document-associated inferred return types with `compute` on the document URI to prevent an inserted type from becoming unreachable by future invalidation.

Learnt from: nixel2007
URL: https://github.com/1c-syntax/bsl-language-server/pull/4402

Timestamp: 2026-08-07T17:52:20.271Z
Learning: `ExpressionTypeInferencer` currently uses shared `ctx.consulted` and `ctx.sawMissing` while nested method return-type computations run. This can over-approximate the method dependency graph and make the incompleteness flag sticky, but it does not produce incorrect inferred types. A separate refactor should isolate nested computation scopes.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

nixel2007 and others added 21 commits August 7, 2026 20:34
На URI в рабочей области приходится ровно один DocumentContext, а сравнение
самих URI нормализует процентные последовательности и заметно дороже: в профиле
analyze по ssl_3_1 percentNormalizedComparison виден отдельной строкой.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Раньше в очередь попадал каждый зависимый метод, чей документ выгружен, — а при
наполнении рабочей области выгружены почти все, и проход перечитывал с диска
половину конфигурации. Теперь метод откладывается, только если расчёт правда
видел непосчитанное значение чужой функции.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Проход после наполнения рабочей области догружал документы и оставлял их в
памяти: на большой конфигурации куча кончалась. Теперь документ возвращается в
прежнее состояние сразу после пересчёта, а догрузка и освобождение идут под
блокировкой документа на запись — иначе у соседнего потока вторичные данные
пропадали бы посреди его расчёта. Одновременно загруженных документов не больше,
чем потоков в пуле.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Перечень состояний вынесен из ServerContext в DocumentState, состояние отдаётся
геттером. Раньше «разобран ли документ» приходилось выяснять перехватом
исключения из getAst().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Проход после наполнения рабочей области на большой конфигурации идёт минутами,
поэтому сообщает прогресс так же, как само наполнение. Слою типов разрешён
доступ к client — ради индикатора.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Проход идёт волнами, и каждая вскрывает новых потребителей: общее число
документов заранее неизвестно, поэтому наращивается по мере того, как работа
находится. Иначе счётчик убегал за границу — «310/104 модулей».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Выведенные по телу типы складываются в SymbolTypeIndex рядом с объявленными,
а наружу торчит один getReturnTypes — и у индекса, и у фасада. Отдельного
хранилища и понятия «расчётный тип» в публичном API больше нет.

Расчёт остался отдельным компонентом MethodReturnTypeIndexer: он слушает событие
разбора, считает и пишет результат. Зависимость на инференсер теперь прямая —
ObjectProvider не нужен, потому что хранилище про инференсер не знает.

Обработчикам события задан явный порядок: без него SymbolTypeIndex шёл последним
и стирал только что записанное индексатором.

Ховер метода показывает тип возвращаемого значения, выведенный по телу, когда
автор его не описал.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Если метод возвращает что-то сверх описанного, под секцией «Возвращаемое
значение» появляется приписка с этими типами: описание могло устареть или быть
неполным, а работает код по своему возврату.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Без общего контекста у расчёта своя нулевая глубина, ограничитель не срабатывает
и цепочка вызовов внутри модуля уходит в рекурсию: на ssl_3_1 это давало 229
переполнений стека.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
В ширину волна пересчитывала всех потребителей разом, и документ попадал под
разбор снова и снова: на ssl_3_1 — 181 разбор на 19 документов, на cpm — 301 на
31. В глубину сначала доводятся значения вызванных методов, потом пересчитывается
вызывающий, поэтому повторов нет, а одновременно нужна лишь текущая цепочка.

Заодно закрыт рассинхрон неэкспортных методов: пересчёт загруженного документа
снимает пометки так же, как это делает разбор, иначе экспортные пересчитывались
по неэкспортным со старыми значениями.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Задачи прохода ждут блокировки документов, а такое ожидание ForkJoinPool
компенсировать не умеет: воркеры вставали, и проход зависал целиком. Работы
здесь на десятки документов, поэтому обход идёт последовательно, в одном потоке.

Индикатор считает то же, по чему тикает, — методы, а не документы: раньше
числитель убегал за знаменатель.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Обход в глубину по методам оказался хуже: грузить всё равно приходится документ
целиком, а обход шёл по методам, и один документ загружался заново под каждый
свой метод — на ssl_3_1 4252 разбора против 181 у прохода по документам.

Теперь единица работы — документ: загрузили, пересчитали все его отложенные
методы, отпустили.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Отложенные методы группируются по документам, документы — по компонентам
сильной связности, компоненты идут в обратном топологическом порядке. Внутри
цикла его документы держатся разобранными разом, но не больше восьми.

Пометка «посчитан» перед пересчётом больше не снимается: соседний метод,
читающий этот прямо сейчас, считал его непосчитанным и снова уходил в
отложенные.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Догрузка документа стирала его записи и пересчитывала с нуля, поэтому
изменившимся выглядел каждый его метод, и весь документ снова уезжал в очередь.
Проход крутил одно и то же до предохранителя: на ssl_3_1 десять волн по 313
методов и 215 разборов на 26 документов.

Теперь при общем проходе разбор в очередь ничего не складывает — что
пересчитывать, решает сам проход по методам, чей расчёт видел непосчитанное.
Стало: одна волна, 66 методов, 18 разборов на 18 документов.

Добавлены отладочные счётчики по волнам и по причинам откладывания.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Каждый вызов перебирал весь индекс имён со startsWith. В профиле analyze по cpm
это была вторая строка сверху: 59 410 сэмплов, а вместе со сравнением строк —
больше, чем у любого другого места. Ответ зависит только от содержимого индекса,
поэтому запоминается и сбрасывается при его изменении.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
…одам

По дампу cpm два графа зависимостей занимали 185 МБ из 278 у всего индекса:
42 253 набора потребителей и 60 127 наборов зависимостей, около 1,26 млн рёбер.
Хранение по методам того не стоит: правка пересчитывает документ целиком, поэтому
связь метода с соседом по тому же файлу не спрашивается никогда, а межфайловые
связи укладываются в число документов.

Заодно закрыт дефект разноса: clear() стирал записи о потребителях метода за шаг
до того, как propagate их спрашивал, — правка модуля не доходила до тех, чьи
значения на нём построены. Теперь снимаются только связи с зависимостями, а
записи о потребителях остаются: содержимое изменилось, но построены они по-прежнему
на этом документе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Quality Gate валился по покрытию нового кода: 61% против требуемых 80%.
Непокрытым оставался порядок обхода документов и вся машинерия прохода —
resolveAll, resolveComponent, withDocumentsLoaded, recomputeLoading.

DocumentDependenciesTest: независимые документы, цепочка, взаимная зависимость,
ссылка на себя, зависимость вне пересчёта, повтор зависимости.

MethodReturnTypeIndexerTest: отложенный метод пересчитывается после наполнения
рабочей области; освобождённый документ догружается ради пересчёта и сразу
отпускается; документы цикла держатся загруженными до неподвижной точки.

Покрытие: DocumentDependencies 0/45 непокрытых, MethodReturnTypeIndexer
25/196 вместо 92/196.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
Разбор объявленных типов: имя типа в описании могло перечислять несколько типов
через запятую, а сравнение брало строку целиком — «Число, Строка» превращалось
в «число,» и не совпадало ни с чем. В ховере из-за этого появлялась приписка
«выведено по коду» там, где автор всё описал.

Выведенные значения по документам: набор вместо CopyOnWriteArrayList. Значение
метода пересчитывается многократно за одно поколение — при разборе, в проходе
доразрешения, при разносе по потребителям, — и список копил повторы, копируя
при каждом добавлении весь массив.

Запись и сброс выведенных значений идут через compute по ключу документа, то
есть под одним замком. Прежде расчёт по запросу, идущий вне событий жизненного
цикла, мог вклиниться между снятием набора и обходом, и значение оставалось в
карте типов без ссылки на него — недостижимым для следующего сброса.

Замки документов в цикле берутся в порядке имён: их держится сразу несколько, и
без общего порядка два потока на пересекающихся компонентах встали бы друг
против друга.

Дерево выражения возврата проверяется на null: на нераспознанном тексте оно не
строится, а контракт вызываемого предполагает непустой узел.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015awbqkuFMyTddXSsVuoHhc
@nixel2007
nixel2007 force-pushed the feat/types-return-types-from-body branch from b1005c7 to ec6b60a Compare August 7, 2026 18:41
@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@nixel2007

Copy link
Copy Markdown
Member Author

Замер после правок по ревью

Ветка перебазирована на свежий develop (в него уже вошли #4412 и #4418). Сравнивались две
сборки одной ветки — до правок по замечаниям и после, — подряд, на вычищенной машине,
конфигурация из 23 575 модулей, куча 16 ГБ, отчёт sarif.

прогон время замечаний
до правок 1233 с 2 979 608
после правок 1130 с 2 979 972
после правок, повтор 1071 с 2 979 980

Замедления нет. Опасение было обоснованным — запись выведенных значений переведена на
compute по ключу документа, а он держит замок корзины и зовётся на каждый расчёт метода. На
деле стало быстрее: замена CopyOnWriteArrayList на набор перевесила стоимость замка — список
копировал весь массив при каждом добавлении, а добавлений сотни тысяч.

Разброс между прогонами одной и той же сборки — 8 замечаний. То есть разница с прежней
сборкой (+364) не шум.

Что именно изменилось. Целиком одна диагностика:

до после
UnknownMember 547 547 547 916

Остальные правила совпадают до единицы.

Причина — правка атомарности в SymbolTypeIndex. Раньше запись и сброс выведенных значений
шли врозь: расчёт по запросу (неэкспортный метод, вне событий жизненного цикла) мог попасть в
промежуток между снятием набора и его обходом, и значение метода терялось — сброс удалял его
из карты типов сразу после записи. Метод оставался без выведенного типа до следующего
пересчёта, а получатель с неизвестным типом диагностику не вызывает.

После правки такой потери нет: у большего числа методов есть посчитанный тип, и проверка
срабатывает там, где раньше молчала. Прибавка — того же рода находки, что и остальные в этом
PR.

@nixel2007

nixel2007 commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Недетерминированность анализа: замеры и разбор

Проверял, стабилен ли результат анализа от прогона к прогону. Короткий ответ: нет, и это не свойство ветки — develop недетерминирован сам по себе. Но ветка размах расхождения увеличивает, и это стоит обсудить до мержа.

Замеры

Один и тот же jar гонялся несколько раз подряд на неизменном исходном коде.

Конфигурация cpm, 16 ГБ, sarif. develop (efa51878e8) — 2 879 079 и 2 879 049, размах 30. Ветка, четыре прогона — 2 979 972 / 2 979 980 / 2 980 003 / 2 979 644, размах 359.

Оговорюсь: по первым двум прогонам ветки размах выходил 8, и это выглядело лучше develop. На четырёх точках видно, что та пара была случайной.

Дальше всё на ssl_3_1 (2162 модуля, 8 ГБ, json) — там расхождение воспроизводится за 80 секунд вместо двадцати минут:

сборка три прогона размах
develop 114 688 / 114 717 / 114 694 29
ветка 116 508 / 116 454 / 116 540 86
ветка + пометка пропущенных документов отложенными 116 534 / 116 550 / 116 535 16
то же, но кэш выражений выключен целиком 116 367 / 116 400 / 116 324 76

По времени ветка быстрее develop (55–79 с против 75–89 с), опытные правки время не ухудшают.

Что именно гуляет

Везде расходятся ровно две диагностики — UnknownMember и EventHandlerInvalidSignature, обе целиком на системе типов. Показательно, что в одной и той же строке разные прогоны ругаются на разные члены:

Catalogs/Пользователи/Forms/ФормаЭлемента/Module.bsl:1438

  • прогон 1 — нет свойства Общие
  • прогон 2 — Общие нашлось, нет ПоказыватьВСпискеВыбора

То есть разбор цепочки доходит до разной глубины: состав Структуры то известен, то нет. Разобрал конкретное место — ЗапретРедактированияРеквизитовОбъектов:97: значение приезжает из чужого общего модуля, объявленный тип той функции — Массив из см. ЗапретРедактированияРеквизитовОбъектов.НовыйБлокируемыйРеквизит, и разрешение уходит по см.-ссылке в третий модуль. Все факты межмодульные.

Причина

Факты о типах продолжают появляться во время самого анализа, а не только при наполнении рабочей области. Расчёт для неэкспортных методов идёт по требованию прямо из разбора документа и пишет в общий индекс, оттуда изменение разносится по потребителям — но те, чья очередь уже прошла, диагностики построили на прежних значениях. Кто успел раньше, решает планировщик, отсюда и разброс.

Что проверено и отпадает:

  • потолок проходов. Отложенный проход сходится за одну волну: Волна 1: пересчитано методов 20, снова отложено 0, в MAX_PASSES он не упирается;
  • кэш выражений. Опытная сборка с полностью выключенным InferredExpressionTypeIndex дала размах не меньше, а больше — 76 против 16. Кэш расхождение не порождает, а гасит: он фиксирует первый посчитанный ответ, тогда как без него каждое чтение видит живое меняющееся состояние. (Ранее я написал здесь обратное — это опровергнуто замером.)

Что подтверждено: в propagaterecomputeLoaded документ с освобождёнными вторичными данными молча пропускается и не помечается отложенным, так что новое значение до его потребителей не доходит. Загружен ли документ в этот момент — зависит от того, сколько их успело осесть в памяти.

Опытная правка, помечающая такой документ отложенным, на ssl_3_1 срезает размах с 86 до 16. Но на cpm выигрыша нет: четыре прогона против четырёх у ветки — 2 979 842 / 2 980 088 / 2 980 133 / 2 979 743, размах 390 против 359. Разницы не видно, обе величины в пределах собственного шума.

Значит на 23 575 модулей решает не пропуск выгруженных документов, а расчёт по требованию во время самого анализа. Правку в ветку не вношу — лечить надо гонку целиком (#4429), а не её частный случай.

Вопрос

Направление лечения выглядит так: доводить типы до неподвижной точки до запуска диагностик, а не полагаться на кэш, который лишь маскирует гонку.

Общая часть выделена в #4429. @nixel2007, вопрос по мержу этого PR: ветка расходится сильнее develop (359 против 30 на cpm) — считаем это блокирующим и чиним до мержа, или мержим и чиним в рамках #4429?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants