Skip to content

Precision gap: DependencyProperty/property-changed old→new subscription rotation flagged as a leak #218

Description

@PhysShell

Found by: the issue #201 oracle sweep, docs/notes/oracle-sweep-2026-07-10.md; catalogued as docs/notes/field-notes-patterns.md entry 11.

The pattern

A property-changed callback (a PropertyChangedCallback or a plain virtual On<X>Changed(old, new) override) manages a subscription across property changes: detach from the old value, attach to the new one, same handler.

private static void OnCommandChanged(CommandTriggerAction action, DependencyPropertyChangedEventArgs e)
{
    if (e.OldValue is ICommand oldCommand)
        oldCommand.CanExecuteChanged -= action.OnCommandCanExecuteChanged;   // unsub OLD
    if (e.NewValue is ICommand newCommand)
        newCommand.CanExecuteChanged += action.OnCommandCanExecuteChanged;   // sub NEW <- flagged OWN001
}

Own.NET pairs a += with a -= per source variablenewCommand has no -= written against it — so it doesn't see that the oldCommand unsubscribe a few lines above is the same rotation slot. There's no leak: at most one subscription is ever live.

Evidence — confirmed in 3 separate real repos, 6+ call sites

  • MahApps.Metro src/MahApps.Metro/Actions/CommandTriggerAction.cs:102-117
  • MaterialDesignInXamlToolkit src/MaterialDesignThemes.Wpf/SmartHint.cs:189-209 (4 findings — IsVisibleChanged/ContentChanged/Loaded/FocusedChanged, same rotation)
  • AvalonEdit ICSharpCode.AvalonEdit/Editing/AbstractMargin.cs:92-101, LineNumberMargin.cs:105-118, Folding/FoldingMargin.cs (a plain virtual OnTextViewChanged(old, new) override, not even a DP callback — same shape)

This is the single most-corroborated false-positive class from the sweep — worth the highest priority among the follow-ups it produced.

Suggested direction (not prescriptive)

Recognise a -=/+= pair as one paired lifecycle when they're against the old/new halves of the same DP-changed callback (or an equivalent On<X>Changed(TValue oldValue, TValue newValue) virtual-method override): same handler expression, unsub on a variable bound from the "old" parameter/branch, sub on a variable bound from the "new" one, within the same method.

Scope

No analyzer code changed as part of the sweep (per the sweep's own guardrails) — this issue exists to track the fix as its own unit of work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions