Skip to content

fix(extractor): recognize WinForms Controls/IContainer disposal channels (Closes #219)#236

Merged
PhysShell merged 3 commits into
mainfrom
claude/winforms-disposal-channels-219
Jul 12, 2026
Merged

fix(extractor): recognize WinForms Controls/IContainer disposal channels (Closes #219)#236
PhysShell merged 3 commits into
mainfrom
claude/winforms-disposal-channels-219

Conversation

@PhysShell

Copy link
Copy Markdown
Owner

Что и зачем

WinForms-поля ложно флагались как OWN001-утечки, потому что field-disposal-скан не моделировал два designer-канала диспоза:

  • (a) Control/ToolStripItem-поле, добавленное в свою Controls/Items-коллекцию, диспозится рекурсивно через Control.Dispose(bool) (контракт фреймворка);
  • (b) компонент, построенный new T(components) или зарегистрированный components.Add(x), диспозится через designer-овский components.Dispose().

~30 own-only FP на одном ShareX; sweep #201, field-notes 12–13.

Фикс — только в экстракторе, без изменений OwnIR: оба канала засчитывают освобождение поля только когда класс достигает disposal-корня — designer-овский Dispose(bool) с base.Dispose(disposing) (ClassReachesDisposalRoot). Класс без такого Dispose (ShareX HistoryItemManager, настоящий баг) — не rooted, его контролы остаются флагнутыми.

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

  • fix — исправление бага (precision / FP)

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

  • python tests/run_tests.py — exit 0 (test_ownir 276/276)
  • ruff check . и mypy --strict на ownlang/ — clean / Success
  • локально через настоящий экстрактор (.NET 8):
WinFormsDisposalSample: 10 findings -> 4
  silent : lblStatus, btnOk, menu, menuItem (a: Controls.Add/AddRange + транзитивный Items), trayIcon (b: new T(components)), icon (b: components.Add)
  flagged: lblForeign (a: чужой контейнер), cms+item (a: класс без Dispose), unregisteredIcon (b: без регистрации)
regression: diff старого/нового по ВСЕМ 49 существующим сэмплам, non-flow И --flow-locals -> БАЙТ-В-БАЙТ идентичен

Не перерасширяет (обязательные негативные контроли — флагнуты)

  • (a) чужой контейнер: добавление в param.Controls (контейнер — параметр, резолвится семантически) → лик.
  • (a) нет disposal-корня: обычный owner без Dispose вообще → его контейнер и дети остаются флагнуты (шейп HistoryItemManager).
  • (b) без регистрации: new NotifyIcon() без components → лик.

Детали

  • (a) собираются рёбра <coll>.Add/AddRange(this.child) с ключом-контейнером ("" = this / имя поля / null = чужой), затем fixpoint: ребёнок rooted-контейнера сам становится rooted (ToolStrip в this.Controls каскадит на свои Items). AddRange по массиву-инициализатору разворачивается.
  • (b) sink — задиспоженное поле типа System.ComponentModel.IContainer; поле, построенное с ним или переданное в .Add(...), засчитывается.

Связанные issue

Closes #219. Найден sweep'ом #201, каталог — field-notes-patterns.md записи 12–13 (помечены shipped). Отдельная зона экстрактора от #218/#225 — можно ревьюить параллельно.

План валидации на живом репозитории (после мержа — отдельной задачей)

Перегнать oracle-sweep по ShareX и показать дельту:

  • ~30 FP должны уйти из own-only: Controls.Add-шейп (~24, напр. ImageViewer.cs:396-399, ColorPicker.cs:84-85, InputBox.cs:145-147) + IContainer-регистрация (~6, напр. TrayForm.cs:41 TrayIcon).
  • 2 подтверждённых реальных бага должны ОСТАТЬСЯ (sweep-заметка): HistoryItemManager_ContextMenu (класс без Dispose — 48 находок) и ShapeManagerMenu (ShapeManager.Dispose() диспозит history, но не menuForm — flagship). Сама перегонка — вне этого PR.

Чеклист

  • изменение покрыто сэмплом + CI-ассертами в обе стороны (silent + controls flagged)
  • README/docs обновлены (field-notes-patterns.md записи 12–13 → shipped)
  • коммиты в conventional-commit стиле (fix:)

🤖 Generated with Claude Code


Generated by Claude Code

…els (#219)

WinForms child fields were false-flagged as OWN001 leaks because the field-disposal
scan didn't model two designer-generated disposal channels:
(a) a Control/ToolStripItem field added to THIS object's own Controls/Items
    collection is disposed transitively by Control.Dispose(bool) (the framework's
    contract);
(b) a component built with `new T(components)` or registered via `components.Add(x)`
    is disposed by the designer's `components.Dispose()`.
~30 own-only FPs on a single ShareX scan; issue #201 sweep, field-notes 12-13.

Fix (extractor-only, no OwnIR change): both channels credit a field's release ONLY
when the class reaches the framework disposal root — a designer `Dispose(bool)`
calling `base.Dispose(disposing)` (ClassReachesDisposalRoot). A class with no such
Dispose (the ShareX HistoryItemManager true-positive) is never rooted, so its owned
controls stay flagged.

- (a) Collect `<coll>.Add/AddRange(this.child)` edges keyed by their container ("" =
  this, a field name, or null = foreign), then fixpoint from the rooted set so a
  ToolStrip added to this.Controls cascades to its Items. AddRange over an array
  initializer is flattened. The container is resolved SEMANTICALLY, so an add into a
  foreign (parameter/local) container yields no credit.
- (b) The IContainer sink is a disposed field of type System.ComponentModel.
  IContainer; a field constructed with it (`new T(components)`) or passed to
  `components.Add(...)` is credited. A component not registered stays flagged.

Precision-first, does not over-widen — negative controls all stay flagged: an add
into a foreign container (lblForeign); a plain owner with no Dispose (cms/item); a
component not container-registered (unregisteredIcon).

Verified locally with the real extractor (.NET 8): WinFormsDisposalSample goes 10
findings -> 4 (six channel-disposed fields silent, four controls flagged); a full
existing-sample diff in BOTH modes (49 samples) is byte-identical (zero regression).
Gates: run_tests.py exit 0, test_ownir 276/276, ruff clean, mypy --strict green.

Closes #219

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cg6DVXahkyY68Ruu7f76rG
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

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

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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: e9b2765d-2f00-4b03-837e-5dbd8d0f68b8

📥 Commits

Reviewing files that changed from the base of the PR and between 6df7717 and e4e7d20.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • docs/notes/field-notes-patterns.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/WinFormsDisposalSample.cs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/winforms-disposal-channels-219

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.

private ContextMenuStrip menu;
private ToolStripMenuItem menuItem;
private NotifyIcon trayIcon;
private NotifyIcon unregisteredIcon; // NEGATIVE CONTROL (b): NOT container-registered
// Controls -> not a disposal channel for this class -> STAYS FLAGGED.
public sealed class ForeignContainerAdder : Control
{
private Label lblForeign;
// (the ShareX HistoryItemManager true-positive shape).
public sealed class MenuOwnerNoDispose
{
private ContextMenuStrip cms = new ContextMenuStrip();
public sealed class MenuOwnerNoDispose
{
private ContextMenuStrip cms = new ContextMenuStrip();
private ToolStripMenuItem item;

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 294a4cd2bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{
if (collExpr is IdentifierNameSyntax { Identifier.Text: "Controls" or "Items" })
return ""; // an inherited `Controls`/`Items` member accessed bare -> this object
if (collExpr is not MemberAccessExpressionSyntax { Name.Identifier.Text: "Controls" or "Items" } coll)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict Items edges to real ToolStrip ownership

Because this accepts any member named Items, a rooted field with a non-owning WinForms item collection is treated like a ToolStrip disposal channel. For example, after this.Controls.Add(combo); combo.Items.Add(this.stream);, the fixpoint below marks stream released, but ComboBox/ListBox item collections do not dispose arbitrary item objects; only ToolStripItemCollection has that ownership behavior. This can hide OWN001 for real owned disposables stored as UI items.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — fixed in e3404af. You're right that ComboBox/ListBox.Items (an ObjectCollection) doesn't dispose its items; only a ToolStripItemCollection does. CollectionContainerKey now gates an Items add on the collection's type being a ToolStripItemCollection (Controls/ControlCollection stays an unconditional channel). Pinned by a new ComboItemForm control: a real disposable stored as a ComboBox item stays flagged even though the combo itself is added to this.Controls.


Generated by Claude Code

// explicit `components.Add(this.G)`.
foreach (var inv in cls.DescendantNodes().OfType<InvocationExpressionSyntax>())
if (inv.Expression is MemberAccessExpressionSyntax { Name.Identifier.Text: "Add" } addMa
&& inv.ArgumentList.Arguments.Count == 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept named IContainer registrations

IContainer also registers components through the named overload Add(IComponent component, string name), but this branch only credits calls with exactly one argument. A designer/custom registration such as components.Add(this.icon, "icon") is therefore ignored, so icon remains reported as OWN001 even though the same components.Dispose() sink will dispose it. Consider using the first argument for both overloads when the receiver is a container sink.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good catch — fixed in e3404af. The (b) branch now accepts Add with >= 1 argument and credits Arguments[0] (the registered IComponent), so the named overload components.Add(this.icon, "name") is recognized just like the single-arg form. Pinned by a new NamedRegistrationForm control (a named-registered component is silent).


Generated by Claude Code

…ner.Add (#219)

Two Codex P2 catches on the #219 disposal-channel fix:

1. Items must be a ToolStripItemCollection. The (a) channel accepted ANY member
   named `Items`, but only a ToolStripItemCollection disposes its items — a
   ComboBox/ListBox `Items` (ObjectCollection) does not. `this.Controls.Add(combo);
   combo.Items.Add(this.stream);` would have wrongly credited a real owned
   disposable stored as a combo item. CollectionContainerKey now gates an `Items`
   add on the collection's type being a ToolStripItemCollection; `Controls`
   (ControlCollection) is always a channel.

2. Named IContainer.Add overload. The (b) channel only credited `components.Add(x)`
   (1 arg), missing the `Add(IComponent, string name)` overload
   (`components.Add(x, "name")`) — the first argument is the registered component in
   both, so credit it whenever the receiver is a container sink.

Pinned by two new controls in WinFormsDisposalSample.cs: `ComboItemForm`
(a disposable stored as a ComboBox item STAYS flagged) and `NamedRegistrationForm`
(a named-registered component is SILENT). ImplementsIContainer now matches
`IContainer` by simple name so the sample's stand-in and the real
System.ComponentModel.IContainer both resolve.

Verified locally: sample now 5 findings (comboItem added to the controls), the
eight channel-disposed fields silent; full existing-sample diff still byte-identical
in both modes (zero regression); gates green.

Refs #219

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cg6DVXahkyY68Ruu7f76rG
public sealed class ComboItemForm : Control
{
private ComboBox combo;
private NotifyIcon comboItem; // a real disposable stored as a combo item
@github-actions

Copy link
Copy Markdown

@coderabbitai review

…sal-channels-219

# Conflicts:
#	.github/workflows/ci.yml
@PhysShell
PhysShell merged commit cae3cd1 into main Jul 12, 2026
36 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.

Precision gap: WinForms Controls/ToolStripItemCollection membership and IContainer registration not recognized as disposal channels

3 participants