From cb2bcc3012174cd824e026231f61b6312e6fcb18 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 10:13:19 +0000 Subject: [PATCH 1/3] fix(extractor): WPF MVVM view-owns-VM exemption (mined FP) + App timer-scope fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two subscription-detector precision fixes from the ScreenToGif mining run. 1. View-owns-its-view-model (the 4 VideoSource OWN001 FPs). A WPF view that CONSTRUCTS its view-model in its own XAML (``) OWNS it — the view<->VM cycle is GC-collectable, so subscribing to the VM's events is not a leak. The extractor parses only `.cs`, where this is invisible (`_vm = DataContext as VM`), so it now reads the sibling `.xaml`: when the view's own XAML inline-constructs its DataContext (a type instantiation, not a Binding/resource reference), a field assigned from `this.DataContext` is folded into the self-owned set, exactly like a `new`'d field. Sound — XAML construction is provable ownership, with no aliasing hole. Mined from ScreenToGif's VideoSource. 2. Scope the App OWN014 exemption to non-timers (CodeRabbit, follow-up to #81). A timer is forced to source "static", so the merged `source == "static" && clsIsApp` guard also swallowed timer findings in `App`; a never-stopped timer there is still a real leak. Gated on `!isTimer`. Regression guards (wpf-extractor): ViewOwnsVmSample.xaml(.cs) — the view owns its XAML-declared VM, must stay silent; InjectedDcViewSample.xaml(.cs) — a BOUND (``) DataContext is not owned, so its subscription must still warn, proving the gate keys off proven construction, not every `DataContext as T`. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED --- .github/workflows/ci.yml | 14 ++++ frontend/roslyn/OwnSharp.Extractor/Program.cs | 80 ++++++++++++++++++- .../roslyn/samples/InjectedDcViewSample.xaml | 8 ++ .../samples/InjectedDcViewSample.xaml.cs | 31 +++++++ frontend/roslyn/samples/ViewOwnsVmSample.xaml | 9 +++ .../roslyn/samples/ViewOwnsVmSample.xaml.cs | 30 +++++++ 6 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 frontend/roslyn/samples/InjectedDcViewSample.xaml create mode 100644 frontend/roslyn/samples/InjectedDcViewSample.xaml.cs create mode 100644 frontend/roslyn/samples/ViewOwnsVmSample.xaml create mode 100644 frontend/roslyn/samples/ViewOwnsVmSample.xaml.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52278707..ace307f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,6 +137,8 @@ jobs: frontend/roslyn/samples/SampleTypes.cs \ frontend/roslyn/samples/PipeFieldsSample.cs \ frontend/roslyn/samples/AppLifetimeSample.cs \ + frontend/roslyn/samples/ViewOwnsVmSample.xaml.cs \ + frontend/roslyn/samples/InjectedDcViewSample.xaml.cs \ -o "$RUNNER_TEMP/facts.json" cat "$RUNNER_TEMP/facts.json" - name: Check facts through the core @@ -292,6 +294,18 @@ jobs: if echo "$out" | grep -q "AppLifetimeSample.cs"; then echo "FAIL: a process-lived App static-event subscription was wrongly reported (OWN014 FP)"; exit 1 fi + # P-004 WPF MVVM ownership (mined: ScreenToGif VideoSource): a view that + # CONSTRUCTS its view-model in its own XAML (`` — read from + # the sibling .xaml) owns it, so a field assigned from `DataContext` is + # self-owned and subscribing to its events is a collectable cycle -> SILENT. + if echo "$out" | grep -q "ViewOwnsVmSample"; then + echo "FAIL: a view that owns its VM via its own XAML DataContext was wrongly reported"; exit 1 + fi + # negative control: a view whose XAML BINDS its DataContext (``) does + # NOT own the VM (it may be externally supplied), so the subscription must + # still WARN — proving the gate keys off proven construction, not every cast. + echo "$out" | grep -qE "InjectedDcViewSample\.xaml\.cs:[0-9]+: warning: \[OWN001\]" \ + || { echo "FAIL: a bound (unowned) DataContext subscription must still warn"; exit 1; } # P-006 DI001 (captive dependency): the registration + constructor graph # extracted from DiCaptiveSample.cs feeds ownlang/di.py. A singleton that # captures a scoped service — directly, transitively through a transient, diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs index 091baec2..7f304517 100644 --- a/frontend/roslyn/OwnSharp.Extractor/Program.cs +++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs @@ -24,6 +24,7 @@ // repo (this is what the `own-check` script / GitHub Action do). using System.Text.Json; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -220,6 +221,49 @@ static bool IsProcessLivedApplication(TypeDeclarationSyntax cls) && cls.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)); } +// P-004 WPF MVVM ownership: a field read from `this.DataContext`, optionally through +// an `as`/cast (`DataContext as VM`, `(VM)DataContext`). Combined with a view whose +// own XAML CONSTRUCTS its DataContext, such a field is the view's owned view-model. +static bool ReadsDataContext(ExpressionSyntax expr) +{ + expr = expr switch + { + BinaryExpressionSyntax b when b.IsKind(SyntaxKind.AsExpression) => b.Left, + CastExpressionSyntax c => c.Expression, + _ => expr, + }; + return expr switch + { + IdentifierNameSyntax id => id.Identifier.Text == "DataContext", + MemberAccessExpressionSyntax m => m.Name.Identifier.Text == "DataContext" + && m.Expression is ThisExpressionSyntax, + _ => false, + }; +} + +// P-004 WPF MVVM ownership: does this XAML construct its own DataContext inline — +// `` — so the view OWNS its +// view-model (a collectable view<->VM cycle)? True only when the property-element's +// child is a TYPE instantiation, not a binding / resource reference (those point at +// an external or inherited value the view does NOT own, so a subscription to it can +// still leak). Matched textually because the extractor has no XAML parser: the +// property-element form `.DataContext>` is what denotes inline construction (the +// `DataContext="{Binding}"` attribute form never matches). Conservative — an +// unrecognised shape yields false (no exemption), never a wrongly-suppressed leak. +static bool XamlDeclaresOwnedDataContext(string xaml) +{ + foreach (Match m in Regex.Matches(xaml, @"\.DataContext\s*>\s*<\s*(?:[\w]+:)?([\w.]+)")) + { + var name = m.Groups[1].Value; + var local = name.Contains('.') ? name[(name.LastIndexOf('.') + 1)..] : name; + if (local is not ("Binding" or "MultiBinding" or "PriorityBinding" + or "StaticResource" or "DynamicResource" or "RelativeSource" + or "TemplateBinding" or "Reference" or "Null")) + return true; + } + return false; +} + // P-004 severity tiering: of the subscriptions that survive the self-owned and // static-handler exemptions (and are not timers), how long-lived is the event // SOURCE? A static event lives for the whole process, so an undetached handler is @@ -1805,6 +1849,12 @@ static bool IsPublicCtor(SyntaxTokenList modifiers) // it under), then build ONE compilation over all of them so the SemanticModel // resolves cross-file and cross-project symbols (P-014 Tier A). var parsed = new List<(string file, SyntaxTree tree)>(); +// P-004 WPF MVVM: source-file paths (`Foo.xaml.cs`) whose sibling `Foo.xaml` constructs +// its own DataContext (``) — the view OWNS that VM. The +// extractor only parses `.cs`, so XAML-declared ownership is invisible from the C# +// alone; we read the sibling `.xaml` here and the subscription detector then treats a +// field assigned from `this.DataContext` as self-owned. Keyed by the tree's FilePath. +var viewsOwningDataContext = new HashSet(StringComparer.Ordinal); foreach (var path in inputs) { // Defensive: an explicit input that is not a readable file (a directory @@ -1827,6 +1877,19 @@ static bool IsPublicCtor(SyntaxTokenList modifiers) continue; } parsed.Add((Rel(path), CSharpSyntaxTree.ParseText(text, path: path))); + if (path.EndsWith(".xaml.cs", StringComparison.OrdinalIgnoreCase)) + { + var xamlPath = path[..^3]; // strip ".cs" -> "....xaml" + try + { + if (File.Exists(xamlPath) && XamlDeclaresOwnedDataContext(File.ReadAllText(xamlPath))) + viewsOwningDataContext.Add(path); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + // An unreadable sibling `.xaml` just means "ownership unknown" — no exemption. + } + } } // Project-local compilation (P-014 Tier A): the framework reference set is this @@ -1936,6 +1999,17 @@ or ImplicitObjectCreationExpressionSyntax && model.GetSymbolInfo(a.Left).Symbol is IFieldSymbol tf && IsTemplatePartFetch(a.Right)) selfOwned.Add(tf.Name); + // * WPF MVVM view-model — `_vm = DataContext as VM`: when THIS view's own + // XAML constructs its DataContext (recorded in viewsOwningDataContext from + // the sibling `.xaml`), the view owns that VM, so the view<->VM cycle is + // collectable and subscribing to its events is not a leak. (Mined from + // ScreenToGif's VideoSource: 4 FP subscriptions to its own declared VM.) + if (viewsOwningDataContext.Contains(tree.FilePath)) + foreach (var a in assigns) + if (a.IsKind(SyntaxKind.SimpleAssignmentExpression) + && model.GetSymbolInfo(a.Left).Symbol is IFieldSymbol dcf + && ReadsDataContext(a.Right)) + selfOwned.Add(dcf.Name); // Is this class the process-lived WPF application object? Used to drop the // static-source region escape (OWN014) — `App` cannot be over-promoted. @@ -1975,8 +2049,10 @@ or ImplicitObjectCreationExpressionSyntax continue; // Process-lived subscriber (the WPF `App` singleton): a static-source // subscription promotes nothing — `App` already lives for the whole - // process — so the region escape (OWN014) is a false positive. - if (source == "static" && clsIsApp) + // process — so the region escape (OWN014) is a false positive. Scoped + // to NON-timers: a timer is forced to source "static" above, but a + // never-stopped timer in `App` is still a real leak (CodeRabbit). + if (!isTimer && source == "static" && clsIsApp) continue; var released = unsub.Contains($"{a.Left}|{a.Right}") || (isTimer && Receiver(a.Left) is { } recv && stopped.Contains(recv)); diff --git a/frontend/roslyn/samples/InjectedDcViewSample.xaml b/frontend/roslyn/samples/InjectedDcViewSample.xaml new file mode 100644 index 00000000..ef30ba31 --- /dev/null +++ b/frontend/roslyn/samples/InjectedDcViewSample.xaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/frontend/roslyn/samples/InjectedDcViewSample.xaml.cs b/frontend/roslyn/samples/InjectedDcViewSample.xaml.cs new file mode 100644 index 00000000..b9ba1bd0 --- /dev/null +++ b/frontend/roslyn/samples/InjectedDcViewSample.xaml.cs @@ -0,0 +1,31 @@ +using System; + +namespace Own.Samples.Wpf; + +// Negative control for the XAML-ownership exemption. This view's XAML BINDS its +// DataContext (`` — see the sibling +// InjectedDcViewSample.xaml): it does NOT construct the VM, the DataContext is +// inherited / externally supplied and may outlive the view. So the field is NOT owned +// and the subscription must still WARN (OWN001). This proves the gate suppresses only +// PROVEN construction, not every `DataContext as T`. +public partial class InjectedDcView +{ + private readonly InjectedVm _vm; + + public InjectedDcView() + { + _vm = DataContext as InjectedVm; + } + + public void OnLoaded() + { + _vm.Changed += OnChanged; // unowned source -> possible leak -> warns + } + + private void OnChanged(object sender, EventArgs e) { } +} + +public class InjectedVm +{ + public event EventHandler Changed; +} diff --git a/frontend/roslyn/samples/ViewOwnsVmSample.xaml b/frontend/roslyn/samples/ViewOwnsVmSample.xaml new file mode 100644 index 00000000..344654a1 --- /dev/null +++ b/frontend/roslyn/samples/ViewOwnsVmSample.xaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/frontend/roslyn/samples/ViewOwnsVmSample.xaml.cs b/frontend/roslyn/samples/ViewOwnsVmSample.xaml.cs new file mode 100644 index 00000000..c704b623 --- /dev/null +++ b/frontend/roslyn/samples/ViewOwnsVmSample.xaml.cs @@ -0,0 +1,30 @@ +using System; + +namespace Own.Samples.Wpf; + +// P-004 WPF MVVM ownership (mined: ScreenToGif VideoSource). This view constructs its +// view-model in its OWN XAML (`<...DataContext>` — see the +// sibling ViewOwnsVmSample.xaml), so it OWNS the VM: the view<->VM reference cycle is +// GC-collectable and subscribing to the VM's events is NOT a leak. The extractor reads +// the sibling `.xaml` to see this (it parses only `.cs` otherwise). Must be SILENT. +public partial class ViewOwnsVm +{ + private readonly OwnedVm _vm; + + public ViewOwnsVm() + { + _vm = DataContext as OwnedVm; + } + + public void OnLoaded() + { + _vm.Changed += OnChanged; // owned source -> collectable cycle -> silent + } + + private void OnChanged(object sender, EventArgs e) { } +} + +public class OwnedVm +{ + public event EventHandler Changed; +} From 72438b9439d1bb11ff3b02af4d6c975e6a4c1239 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 10:19:04 +0000 Subject: [PATCH 2/3] fix(extractor): restrict XAML DataContext-ownership to the ROOT element (Codex P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XamlDeclaresOwnedDataContext matched ANY `*.DataContext` property-element in the sibling XAML. But the code-behind's `this.DataContext` is the ROOT element's, so a nested `` (a different element's DataContext) would wrongly mark the whole `.xaml.cs` as owning — silently dropping a real OWN001 on the root's injected/bound VM. Now: strip comments / processing-instructions, find the root element tag, and match only `` with a constructed (type-instantiation) child. The InjectedDcView control XAML gains exactly this shape — a BOUND root DataContext plus a nested `` — so the existing "must warn" assertion now also guards the root-restriction (without it the nested construction would make the view look owned and the subscription would be wrongly suppressed). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED --- frontend/roslyn/OwnSharp.Extractor/Program.cs | 33 ++++++++++++++----- .../roslyn/samples/InjectedDcViewSample.xaml | 14 ++++++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs index 7f304517..33891fb4 100644 --- a/frontend/roslyn/OwnSharp.Extractor/Program.cs +++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs @@ -242,17 +242,32 @@ BinaryExpressionSyntax b when b.IsKind(SyntaxKind.AsExpression) => b.Left, } // P-004 WPF MVVM ownership: does this XAML construct its own DataContext inline — -// `` — so the view OWNS its -// view-model (a collectable view<->VM cycle)? True only when the property-element's -// child is a TYPE instantiation, not a binding / resource reference (those point at -// an external or inherited value the view does NOT own, so a subscription to it can -// still leak). Matched textually because the extractor has no XAML parser: the -// property-element form `.DataContext>` is what denotes inline construction (the -// `DataContext="{Binding}"` attribute form never matches). Conservative — an -// unrecognised shape yields false (no exemption), never a wrongly-suppressed leak. +// `` — so the view OWNS its view-model +// (a collectable view<->VM cycle)? Restricted to the ROOT element's DataContext: the +// code-behind's `this.DataContext` is the root's, so only the root's own constructed +// DataContext proves ownership. A NESTED element's `` sets +// a DIFFERENT element's DataContext and must NOT exempt the view (Codex) — else a real +// leak on the root's injected VM is silently dropped. True only when the root's +// property-element child is a TYPE instantiation, not a binding / resource reference +// (those point at an external or inherited value the view does NOT own). Matched +// textually (no XAML parser): the property-element form `.DataContext>` denotes inline +// construction; the `DataContext="{Binding}"` attribute form never matches. +// Conservative — an unrecognised shape yields false (no exemption), never a +// wrongly-suppressed leak. static bool XamlDeclaresOwnedDataContext(string xaml) { - foreach (Match m in Regex.Matches(xaml, @"\.DataContext\s*>\s*<\s*(?:[\w]+:)?([\w.]+)")) + // Strip comments / processing instructions so `` text inside a + // comment (or ``) cannot be mistaken for markup, nor skew the root search. + xaml = Regex.Replace(xaml, @"", " ", RegexOptions.Singleline); + xaml = Regex.Replace(xaml, @"<\?.*?\?>", " ", RegexOptions.Singleline); + // The root element is the view type (its x:Class is the code-behind). Find its tag + // (the first element open — not ``), then match ONLY ``. + var rootMatch = Regex.Match(xaml, @"<\s*([A-Za-z_][\w:]*)"); + if (!rootMatch.Success) + return false; + var root = Regex.Escape(rootMatch.Groups[1].Value); + foreach (Match m in Regex.Matches(xaml, + $@"<\s*{root}\s*\.\s*DataContext\s*>\s*<\s*(?:[\w]+:)?([\w.]+)")) { var name = m.Groups[1].Value; var local = name.Contains('.') ? name[(name.LastIndexOf('.') + 1)..] : name; diff --git a/frontend/roslyn/samples/InjectedDcViewSample.xaml b/frontend/roslyn/samples/InjectedDcViewSample.xaml index ef30ba31..a070b9f4 100644 --- a/frontend/roslyn/samples/InjectedDcViewSample.xaml +++ b/frontend/roslyn/samples/InjectedDcViewSample.xaml @@ -1,8 +1,18 @@ - + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:local="clr-namespace:Own.Samples.Wpf"> + + + + + + + From bed4868fa0cbb7b11f6627a136fb64b9423f3222 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 10:23:24 +0000 Subject: [PATCH 3/3] refactor(extractor): parse XAML structurally for DataContext ownership (CodeRabbit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the textual regex in XamlDeclaresOwnedDataContext with structural XML parsing (XDocument), per CodeRabbit's recommendation. Strictly more correct: it inspects only the ROOT element's own `` (a direct child, in the root's namespace) and excludes the entire `x:` language namespace — so x:Static / x:Null / x:Reference (external or shared values the view does NOT own) no longer count as construction, closing a residual false-negative the regex left open. A malformed `.xaml` parses to false (conservative). Drops the now-unused Regex import. Also tighten the InjectedDcView CI assertion to require the OWN001 warning AND the injected-source wording on that file's line specifically (CodeRabbit nitpick), so the negative control cannot be satisfied by an unrelated finding. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED --- .github/workflows/ci.yml | 4 +- frontend/roslyn/OwnSharp.Extractor/Program.cs | 57 ++++++++----------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ace307f8..13b3fdaf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,8 +304,8 @@ jobs: # negative control: a view whose XAML BINDS its DataContext (``) does # NOT own the VM (it may be externally supplied), so the subscription must # still WARN — proving the gate keys off proven construction, not every cast. - echo "$out" | grep -qE "InjectedDcViewSample\.xaml\.cs:[0-9]+: warning: \[OWN001\]" \ - || { echo "FAIL: a bound (unowned) DataContext subscription must still warn"; exit 1; } + echo "$out" | grep -qE "InjectedDcViewSample\.xaml\.cs:[0-9]+: warning: \[OWN001\].*injected dependency whose lifetime is unknown" \ + || { echo "FAIL: a bound (unowned) DataContext subscription must still warn with the injected-source wording"; exit 1; } # P-006 DI001 (captive dependency): the registration + constructor graph # extracted from DiCaptiveSample.cs feeds ownlang/di.py. A singleton that # captures a scoped service — directly, transitively through a transient, diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs index 33891fb4..a05fb7aa 100644 --- a/frontend/roslyn/OwnSharp.Extractor/Program.cs +++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs @@ -24,7 +24,7 @@ // repo (this is what the `own-check` script / GitHub Action do). using System.Text.Json; -using System.Text.RegularExpressions; +using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -243,40 +243,33 @@ BinaryExpressionSyntax b when b.IsKind(SyntaxKind.AsExpression) => b.Left, // P-004 WPF MVVM ownership: does this XAML construct its own DataContext inline — // `` — so the view OWNS its view-model -// (a collectable view<->VM cycle)? Restricted to the ROOT element's DataContext: the -// code-behind's `this.DataContext` is the root's, so only the root's own constructed -// DataContext proves ownership. A NESTED element's `` sets -// a DIFFERENT element's DataContext and must NOT exempt the view (Codex) — else a real -// leak on the root's injected VM is silently dropped. True only when the root's -// property-element child is a TYPE instantiation, not a binding / resource reference -// (those point at an external or inherited value the view does NOT own). Matched -// textually (no XAML parser): the property-element form `.DataContext>` denotes inline -// construction; the `DataContext="{Binding}"` attribute form never matches. -// Conservative — an unrecognised shape yields false (no exemption), never a -// wrongly-suppressed leak. +// (a collectable view<->VM cycle)? Parsed structurally (XAML is XML), restricted to the +// ROOT element's own DataContext: the code-behind's `this.DataContext` is the root's, so +// a nested `` (a different element's) must NOT exempt the +// view (Codex / CodeRabbit) — else a real leak on the root's injected VM is dropped. +// True only when the root's DataContext child is a constructed object that the view owns: +// NOT a binding / resource reference, and NOT an `x:`-namespace language object +// (`x:Static`, `x:Null`, `x:Reference`, ...), which name an external/shared value. A +// malformed `.xaml` yields false (conservative — no exemption, never a dropped leak). static bool XamlDeclaresOwnedDataContext(string xaml) { - // Strip comments / processing instructions so `` text inside a - // comment (or ``) cannot be mistaken for markup, nor skew the root search. - xaml = Regex.Replace(xaml, @"", " ", RegexOptions.Singleline); - xaml = Regex.Replace(xaml, @"<\?.*?\?>", " ", RegexOptions.Singleline); - // The root element is the view type (its x:Class is the code-behind). Find its tag - // (the first element open — not ``), then match ONLY ``. - var rootMatch = Regex.Match(xaml, @"<\s*([A-Za-z_][\w:]*)"); - if (!rootMatch.Success) + XDocument doc; + try { doc = XDocument.Parse(xaml); } + catch (System.Xml.XmlException) { return false; } + var root = doc.Root; + if (root is null) return false; - var root = Regex.Escape(rootMatch.Groups[1].Value); - foreach (Match m in Regex.Matches(xaml, - $@"<\s*{root}\s*\.\s*DataContext\s*>\s*<\s*(?:[\w]+:)?([\w.]+)")) - { - var name = m.Groups[1].Value; - var local = name.Contains('.') ? name[(name.LastIndexOf('.') + 1)..] : name; - if (local is not ("Binding" or "MultiBinding" or "PriorityBinding" - or "StaticResource" or "DynamicResource" or "RelativeSource" - or "TemplateBinding" or "Reference" or "Null")) - return true; - } - return false; + // The root's OWN `` property-element (a direct child of the root, + // in the root's namespace) — not a nested element's, not another property. + var dc = root.Element(root.Name.Namespace + (root.Name.LocalName + ".DataContext")); + var child = dc?.Elements().FirstOrDefault(); + if (child is null) + return false; + if (child.Name.NamespaceName == "http://schemas.microsoft.com/winfx/2006/xaml") + return false; // x:Static / x:Null / x:Reference / x:Type / ... + return child.Name.LocalName is not ("Binding" or "MultiBinding" or "PriorityBinding" + or "StaticResource" or "DynamicResource" or "RelativeSource" + or "TemplateBinding" or "Reference" or "Null"); } // P-004 severity tiering: of the subscriptions that survive the self-owned and