You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Research: OWN001 [disposable field] ignores disposal in another partial file (*.Designer.cs) — bounded experiment #410
Исследовательский эксперимент (bounded), а не одобренная фича. Решение о production-изменении принимается только после gate G1–G4.
Наблюдение
При разметке находок в #407 восемь из 22 ложных OWN001 [disposable field] оказались полями, которые освобождаются в другом partial-файле того же класса:
FileStatusList._customDiffToolsSequence (объявлено в FileStatusList.ContextMenu.cs)
FileStatusList.cs, лямбда Disposed += … в конструкторе
Предполагаемые механизмы — гипотезы, проверяются в G1:
IsGenerated() (Program.cs:222) безусловно пропускает *.Designer.cs при разборе входа: эти файлы не попадают в компиляцию, и доказательство освобождения в них невидимо.
Проход по полям собирает освобождения внутри одного ClassDeclarationSyntax, а не во всех partial-объявлениях класса.
Ограничения эксперимента
*.Designer.csне включаются в обычную генерацию находок. Они читаются только как дополнительный источник доказательств освобождения.
Освобождение засчитывается только при конкретной связи с полем: вызов Dispose на этом поле, связанный по символу. components.Dispose() и Dispose «чего-нибудь» не освобождают произвольное поле класса.
Статус: GO на ограниченную доработку PoC, production gate pending G3.
Критерий
Результат
G1, причина
PASS. Главная причина — освобождения ищутся только внутри одного объявления класса (ClassDeclarationSyntax). Пропуск *.Designer.cs (IsGenerated) — вторая причина, только для designer-файлов.
Вариант A: designer-файл в компиляции
KILL по G4
Вариант B: известные FP
PASS
G3, идентичность поля
PASS
G3, достижимость освобождения
не пройден
G4, корпус
PASS по проверенным случаям
G1
Если Dispose перенести в файл с другим именем, который попадает в компиляцию, OWN001 всё равно остаётся.
То же происходит, когда освобождение находится в обычном partial-файле: в лямбде Disposed +=.
Если освобождение лежит в том же объявлении класса, OWN001 исчезает.
Вариант A — KILL
Designer-файл добавлялся в компиляцию только как источник доказательств, без генерации фактов. На gitextensions с global using вариант снимает 8 целевых FP. Но designer-файлы разрешают типы дочерних контролов, и 43 OWN050 превращаются в 75 новых предупреждений о неотписанных событиях дочерних контролов. Скорее всего это FP, и это регрессия вне целевой группы.
Вариант B
Другие partial-объявления. Освобождение засчитывается, только если получатель по символу — поле этого класса.
Designer-файлы. Файл Foo.Designer.cs рядом с Foo.cs разбирается, но не компилируется. Класс сопоставляется по полному имени, поле ищется среди членов класса, перекрытие локалом или параметром исключено.
G2. На gitextensions с тем же GlobalUsings.cs, что в #407, сняты ровно 8 находок — все 8 размечены там как FP. Новых находок 0.
G3, идентичность поля — PASS. OWN001 остаётся во всех контрольных случаях:
одноимённое поле другого класса;
освобождается другое поле;
освобождается поле другого экземпляра;
поле перекрыто локалом;
designer-файл в другом пространстве имён.
G3, достижимость — не пройден.Dispose в невызываемом методе другого partial-файла подавляет OWN001. main ведёт себя так же внутри одного объявления, но это не оправдывает распространение ошибки на partial-файлы.
G4. На корпусе из 144 деревьев без global using 140 деревьев побайтно совпадают с main. В остальных снято 5 уникальных находок, добавлено 0. Все 5 проверены по исходнику: поле освобождается в другом partial-файле.
Итого: снято 13 подтверждённых FP в 4 репозиториях (8 — в условиях #407, 5 — на обычном корпусе). Новых находок нет, настоящих предупреждений не потеряно.
Следующий шаг
Сузить подавление до доказанных путей жизненного цикла. Нужно проверить три случая, где освобождение лежит в другом partial-файле:
освобождение из настоящего метода жизненного цикла — OWN001 должен исчезнуть;
освобождение только в невызываемом методе — OWN001 должен остаться;
освобождение за условием, которое не гарантировано, — OWN001 должен остаться.
Часть из 13 снятых FP при этом может вернуться; это допустимо.
Статус: G1–G4 PASS; GO for production PR (заменяет «production gate pending G3»). Merge — только после регрессионных тестов и CI.
Что показал G3
В раунде 1 G3 был провален сильнее, чем сообщалось. Чтобы это проверить, взяты 14 случаев: поле объявлено в Foo.cs, а освобождается в другом файле. Вариант B раунда 1 подавлял предупреждение во всех 8 случаях, где оно должно остаться, а не только для невызываемого метода.
Как сужено
partial-объявления. Освобождение засчитывается, только если:
оно находится в доказанном контексте завершения. Используется уже существующая модель TeardownContextMethods / InTeardownContext — на ней работают проверки -= и Stop(). Подходят Dispose/DisposeAsync/OnClosed…, всё, что из них вызывается по разрешённым вызовам внутри того же объявления, и лямбда, подписанная на собственный Disposed/Closed;
каждое охватывающее условие — это положительный if (disposing) или проверка того же поля на null. Ветка else, флаг или цикл не подходят.
designer-файлы. Вызов должен стоять прямо в теле метода завершения, без лямбд и помощников. disposing должен быть собственным bool-параметром этого метода. Ограничения на условия те же.
Результаты
Случай
main
раунд 1
раунд 2
ожидание
P1 Dispose() в partial
WARN
—
—
—
P2 невызываемый UnusedCleanup()
WARN
—
WARN
WARN
P3 if (_enabled) в Dispose
WARN
—
WARN
WARN
P4 Dispose() → Cleanup()
WARN
—
—
—
P5 лямбда на собственный Disposed
WARN
—
—
—
P6 if (_f != null)
WARN
—
—
—
P7 лямбда на Disposed чужого объекта
WARN
—
WARN
WARN
P8 неподписанная лямбда в Dispose
WARN
—
WARN
WARN
D1 designer if (disposing) { _f.Dispose(); }
WARN
—
—
—
D2 designer, невызываемый метод
WARN
—
WARN
WARN
D3 designer if (!disposing)
WARN
—
WARN
WARN
D4 designer, ветка else
WARN
—
WARN
WARN
D5 designer if (_enabled)
WARN
—
WARN
WARN
D6 designer, лямбда в Dispose
WARN
—
WARN
WARN
G3. Раунд 2 проходит все 14 случаев. Контроли идентичности поля тоже проходят: чужой класс, другое поле, другой экземпляр, перекрывающий локал, другое пространство имён.
G2. gitextensions с тем же GlobalUsings.cs, что в #407: сняты те же 8 размеченных FP, новых находок 0.
G4. На 144 деревьях без global using 140 побайтно совпадают с main. Снято 4 находки вместо прежних 5, добавлено 0.
Вернулось предупреждение на garnet ReplicationManager.resetHandler. Поле освобождается и пересоздаётся на пути восстановления реплики (ReplicaDiskbasedSync.cs:211), а не при завершении владельца, поэтому предупреждение остаётся обоснованно.
Остались снятыми:
honua _operationCancellation — Dispose после if (_disposed) return;;
garnet ctsGossip — DisposeBackgroundTasks, вызывается из завершения;
rabbitmq _recoveryCancellationTokenSource и _mainLoopCts — пути Dispose/finally при закрытии.
Итого: 12 подтверждённых FP снято в 4 репозиториях, новых находок нет.
Сознательные ограничения
Модель остаётся консервативной:
цепочка вызовов не выходит за пределы одного partial-объявления;
в designer-файлах помощники не учитываются.
Часть FP поэтому останется, но необоснованного доказательства освобождения модель не даёт.
Результат не относится к #407: он показывает, что OWN001 можно сделать точнее, но не доказывает, что компиляция с учётом проекта окупается.
Activity
PhysShell commentedon Oct 18, 2026
Итог эксперимента, раунд 1
Статус: GO на ограниченную доработку PoC, production gate pending G3.
ClassDeclarationSyntax). Пропуск*.Designer.cs(IsGenerated) — вторая причина, только для designer-файлов.G1
Disposeперенести в файл с другим именем, который попадает в компиляцию, OWN001 всё равно остаётся.Disposed +=.Вариант A — KILL
Designer-файл добавлялся в компиляцию только как источник доказательств, без генерации фактов. На gitextensions с
global usingвариант снимает 8 целевых FP. Но designer-файлы разрешают типы дочерних контролов, и 43 OWN050 превращаются в 75 новых предупреждений о неотписанных событиях дочерних контролов. Скорее всего это FP, и это регрессия вне целевой группы.Вариант B
Foo.Designer.csрядом сFoo.csразбирается, но не компилируется. Класс сопоставляется по полному имени, поле ищется среди членов класса, перекрытие локалом или параметром исключено.G2. На gitextensions с тем же
GlobalUsings.cs, что в #407, сняты ровно 8 находок — все 8 размечены там как FP. Новых находок 0.G3, идентичность поля — PASS. OWN001 остаётся во всех контрольных случаях:
G3, достижимость — не пройден.
Disposeв невызываемом методе другого partial-файла подавляет OWN001.mainведёт себя так же внутри одного объявления, но это не оправдывает распространение ошибки на partial-файлы.G4. На корпусе из 144 деревьев без
global using140 деревьев побайтно совпадают сmain. В остальных снято 5 уникальных находок, добавлено 0. Все 5 проверены по исходнику: поле освобождается в другом partial-файле.FileBackedLicenseService._operationCancellationFileBackedLicenseService.cs:87ClusterManager.ctsGossipClusterManager.cs:167ReplicationManager.resetHandlerReplicaDiskbasedSync.cs:211AutorecoveringConnection._recoveryCancellationTokenSourceAutorecoveringConnection.cs:368Connection._mainLoopCtsConnection.cs:767Итого: снято 13 подтверждённых FP в 4 репозиториях (8 — в условиях #407, 5 — на обычном корпусе). Новых находок нет, настоящих предупреждений не потеряно.
Следующий шаг
Сузить подавление до доказанных путей жизненного цикла. Нужно проверить три случая, где освобождение лежит в другом partial-файле:
Часть из 13 снятых FP при этом может вернуться; это допустимо.
PoC не закоммичен.
PhysShell commentedon Oct 18, 2026
Раунд 2: достижимость освобождения
Статус: G1–G4 PASS; GO for production PR (заменяет «production gate pending G3»). Merge — только после регрессионных тестов и CI.
Что показал G3
В раунде 1 G3 был провален сильнее, чем сообщалось. Чтобы это проверить, взяты 14 случаев: поле объявлено в
Foo.cs, а освобождается в другом файле. Вариант B раунда 1 подавлял предупреждение во всех 8 случаях, где оно должно остаться, а не только для невызываемого метода.Как сужено
partial-объявления. Освобождение засчитывается, только если:
TeardownContextMethods/InTeardownContext— на ней работают проверки-=иStop(). ПодходятDispose/DisposeAsync/OnClosed…, всё, что из них вызывается по разрешённым вызовам внутри того же объявления, и лямбда, подписанная на собственныйDisposed/Closed;if (disposing)или проверка того же поля на null. Веткаelse, флаг или цикл не подходят.designer-файлы. Вызов должен стоять прямо в теле метода завершения, без лямбд и помощников.
disposingдолжен быть собственным bool-параметром этого метода. Ограничения на условия те же.Результаты
mainDispose()в partialUnusedCleanup()if (_enabled)в DisposeDispose()→Cleanup()Disposedif (_f != null)Disposedчужого объектаif (disposing) { _f.Dispose(); }if (!disposing)if (_enabled)G3. Раунд 2 проходит все 14 случаев. Контроли идентичности поля тоже проходят: чужой класс, другое поле, другой экземпляр, перекрывающий локал, другое пространство имён.
G2. gitextensions с тем же
GlobalUsings.cs, что в #407: сняты те же 8 размеченных FP, новых находок 0.G4. На 144 деревьях без
global using140 побайтно совпадают сmain. Снято 4 находки вместо прежних 5, добавлено 0.ReplicationManager.resetHandler. Поле освобождается и пересоздаётся на пути восстановления реплики (ReplicaDiskbasedSync.cs:211), а не при завершении владельца, поэтому предупреждение остаётся обоснованно._operationCancellation— Dispose послеif (_disposed) return;;ctsGossip—DisposeBackgroundTasks, вызывается из завершения;_recoveryCancellationTokenSourceи_mainLoopCts— пути Dispose/finally при закрытии.Итого: 12 подтверждённых FP снято в 4 репозиториях, новых находок нет.
Сознательные ограничения
Модель остаётся консервативной:
Часть FP поэтому останется, но необоснованного доказательства освобождения модель не даёт.
Результат не относится к #407: он показывает, что OWN001 можно сделать точнее, но не доказывает, что компиляция с учётом проекта окупается.
fix(p037): the companion Foo.Designer.cs is a semantic side input (#410)
Merge pull request #411 from PhysShell/fix/410-partial-dispose