Skip to content

fix(roslyn): a field released in another partial file at teardown is not a leak (#410) - #411

Merged
PhysShell merged 2 commits into
mainfrom
fix/410-partial-dispose
Oct 18, 2026
Merged

PhysShell merged 2 commits into
mainfrom
fix/410-partial-dispose

Conversation

@PhysShell

@PhysShell PhysShell commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Что и зачем

OWN001 [disposable field] искал _f.Dispose() только внутри того объявления класса, где объявлено поле. Кроме того, файлы *.Designer.cs не разбираются вовсе (IsGenerated). Из-за этого поле WinForms, освобождаемое в designer-методе Dispose(bool), или поле, объявленное в Foo.ContextMenu.cs и освобождаемое в Foo.cs, считалось неосвобождённым.

Исправление сделано по итогам эксперимента в #410 (раунды 1 и 2). Затронут только экстрактор; OwnIR и ядра не менялись.

Другие partial-объявления класса. Освобождение поля засчитывается при выполнении всех условий:

  • вызов Dispose / DisposeAsync / Close (или вызов, освобождающий получателя) сделан на поле этого класса — проверяется по символу;
  • вызов достижим при завершении объекта. Используется существующая модель TeardownContextMethods / InTeardownContext: метод завершения этого объявления, метод, который он доказуемо вызывает, или обработчик собственного Disposed/Closed;
  • каждое охватывающее условие — положительный if (disposing) или проверка того же поля на null;
  • нет раннего return, зависящего от параметра.

Файл Foo.Designer.cs рядом с Foo.cs. Он разбирается, но не компилируется. Объявление того же класса, найденное по полному имени, служит только синтаксическим доказательством освобождения:

  • вызов стоит прямо в методе завершения;
  • имя — поле этого класса и не перекрыто локалом;
  • к условиям применяется то же правило.

Источником фактов Designer-файл не становится. Вариант с компиляцией Designer-файла отвергнут (G4 в #410): на gitextensions он превращал 43 предупреждения OWN050 в 75 вероятных ложных находок о подписках.

OWN001 сохраняется, если освобождение:

  • находится в невызываемом методе;
  • стоит за флагом;
  • стоит в ветке финализатора или else;
  • находится в неподписанной лямбде;
  • подписано на Disposed чужого объекта;
  • является освобождением с пересозданием на пути восстановления.

Тип изменения

  • feat — новая возможность
  • fix — исправление бага
  • docs — документация
  • refactor / chore / test / ci — новая регрессия + шаг CI

Как проверено

  • OWN_TIERB_REQUIRED=1 python tests/test_extractor_partial_dispose.py — 21 случай + проверка, что Designer-файл не даёт фактов, в режимах plain и --flow-locals:
    • 14 случаев достижимости;
    • аналог garnet resetHandler;
    • контроли идентичности поля: другой экземпляр, перекрывающий локал в partial- и Designer-файле, чужой класс, другое поле, Designer-файл в другом пространстве имён.
  • Тест красный на исправлении, выключенном мутантом. Все 7 мутантов пойманы: без partial, без Designer, без достижимости, условия всегда «ок», любой метод Designer, без проверки перекрытия, любой символ вместо поля.
  • Факты побайтно совпадают с main на samples, protocol-samples, heap-effects-samples и project-input-sample, в режимах plain и --flow-locals. Исключение — protocol-samples с --flow-locals: там экстрактор и на main, и с исправлением завершается с кодом 2.
  • Корпус из 144 реальных деревьев: 140 побайтно совпадают с main. В 4 деревьях снято 5 находок — 4 уникальных, honua-server даёт по одной и в base, и в fix. Новых находок 0. Все 4 проверены по исходнику: поле освобождается при завершении в другом partial-файле (honua _operationCancellation, garnet ctsGossip, rabbitmq _recoveryCancellationTokenSource и _mainLoopCts).
  • gitextensions с тем же GlobalUsings.cs, что в Extractor: project-aware semantic resolution — types from SDK implicit usings do not resolve #407: сняты 8 находок, все размечены там как FP; новых 0.
  • Без регрессий в OWN_TIERB_REQUIRED=1:
    • test_extractor_sibling_locals, test_extractor_deep_flow, test_extractor_columns;
    • test_guarded_consume, test_ownir (292/292), test_certify (114/114);
    • test_p037_sidecar, test_stage3_surfaces.
  • ruff check . — чисто.
  • python tests/run_tests.py целиком локально не проходит из-за окружения — так же, как в fix(roslyn): a deep if/else-if chain no longer aborts the whole extraction (#401) #403 и fix(roslyn): same-named locals in sibling scopes are no longer conflated (#408) #409.

P-037: Designer-файл как семантический побочный вход (08142f34)

Первый прогон CI на 0c179d52 упал на extractor-fs-sites-all-classified в tests/test_p037_evidence.py. Новые места чтения File.Exists(designer) и File.ReadAllText(designer) не были классифицированы.

Одной классификации было бы недостаточно: Designer-файл менял бы факты, не меняя записанное evidence. Поэтому сделано следующее:

  • Механизм. scripts/p037_evidence.py: добавлен пятый механизм explicit-file sibling-designer, привязанный к конструкции экстрактора.
  • support_manifest. Туда попадает парный Foo.Designer.cs каждого основного Foo.cs, если сам Designer-файл не основной вход. Если основной — его уже фиксирует analysis_manifest, а двойная материализация сломала бы population_intact.
  • tests/test_p037_evidence.py. Оба места чтения классифицированы под этим механизмом. Добавлены контроли на синтетической истории:
    • Designer-файл входит в состав support-файлов, когда основной вход только Foo.cs;
    • Designer-файл не дублируется, когда он сам основной вход;
    • правка Designer-файла делает прежнюю запись недействительной («recorded support_manifest does not match»);
    • та же правка ломает сравнение before/after («semantic support manifests differ»), не меняя основной манифест.
  • Проверка контролей. Мутант, убирающий Designer-файл из support_manifest, роняет 3 новых контроля.
  • Состав support-файлов репозитория не изменился, их по-прежнему три. Каждый *.Designer.cs под frontend/ и audit/ лежит рядом со своим Foo.cs в том же корне, поэтому он основной вход. Проверка repo-support-closure-is-exactly-three не трогалась.
  • Локальные проверки. test_p037_evidence и test_p037_sidecar проходят на чистом дереве; ruff чист; ошибок mypy в p037 нет (11 прежних в perf_baseline.py / envcapture.py).

Связанные issue

Closes #410

Чеклист

  • изменение покрыто тестом/селфтестом (или объяснено, почему нет)
  • README/docs обновлены при необходимости — не требуется
  • коммиты в conventional-commit стиле (feat:, fix:, docs: …)

Границы исправления

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

  • Замыкание по вызовам не выходит за пределы одного partial-объявления. Если Dispose в одном файле вызывает помощника из другого, освобождение не засчитывается.
  • В Designer-файлах помощники не учитываются.
  • Учитывается только парный Foo.Designer.cs рядом с разобранным Foo.cs.
  • Освобождение через локал-алиас в другом partial-файле не учитывается.
  • Само объявление поля по-прежнему следует старой политике: освобождение где угодно в классе. Эта разница с partial-путём оставлена намеренно и вне скоупа.

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

🤖 Generated with Claude Code

PhysShell and others added 2 commits October 18, 2026 12:16
…not a leak (#410)

OWN001 [disposable field] looked for `_f.Dispose()` only inside the one
class declaration that declares the field, and *.Designer.cs files are
never parsed. A WinForms field released in the designer Dispose(bool), or
a field declared in Foo.ContextMenu.cs and disposed in Foo.cs, was
reported as never disposed.

- Other partial declarations: a Dispose/DisposeAsync/Close (or a
  receiver-releasing call) whose receiver binds to a field of THIS class
  is credited when it is reached at this object's teardown: a teardown
  method of that declaration or a method it provably calls, or a handler
  wired to our own Disposed/Closed (the existing TeardownContextMethods /
  InTeardownContext model), with every enclosing branch either the
  positive `if (disposing)` or a null check of the field, and no
  parameter-guarded early return.
- A companion Foo.Designer.cs is parsed but not compiled (compiling it
  resolves the designer-declared child controls and turned OWN050
  advisories into subscription findings on gitextensions). Its
  declarations of the same qualified class are read only as syntactic
  release evidence: the call directly in a teardown method, the name a
  field of the class and not shadowed, the same guard rule. It never
  becomes a source of facts.

A release in an uncalled method, behind a flag, in the finalizer branch,
in an unwired lambda or on another object's Disposed keeps OWN001.

Closes #410

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The #410 extractor change reads the `Foo.Designer.cs` beside a primary
`Foo.cs` as release evidence. P-037's fitness pin refused the two new
unclassified read sites, and rightly: classifying them alone would let
the designer file change the facts without changing recorded evidence.

- p037_evidence.py: a fifth explicit-file mechanism, `sibling-designer`,
  anchored to the extractor construct; support_manifest carries the
  companion designer of every primary Foo.cs that is not itself a
  primary input (when it is, the analysis manifest already pins it and
  materializing it twice would break population_intact).
- test_p037_evidence.py: the two read sites classified under it, and
  controls on a synthetic history: the designer enters the closure with
  only Foo.cs as primary input, is not duplicated when it is primary,
  and a designer edit invalidates the earlier record and breaks the
  before/after comparison while leaving the primary manifest unchanged.

The repository population's support closure is unchanged (exactly three):
every designer file under frontend/ and audit/ sits beside its Foo.cs in
the same root, so it is a primary input.

Refs #410

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PhysShell
PhysShell merged commit 2fefb19 into main Oct 18, 2026
83 of 84 checks passed
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.

Research: OWN001 [disposable field] ignores disposal in another partial file (*.Designer.cs) — bounded experiment

1 participant