Skip to content

Commit e3ec4fc

Browse files
committed
fix(extractor): resolve AssociatedObject's binding before exempting it (#227, Codex P2)
IsAssociatedObjectAccess matched the identifier `AssociatedObject` purely by text, so a local, a parameter, or a hidden member DECLARED in the Behavior subclass named `AssociatedObject` — holding an injected publisher, not the inherited accessor — would wrongly pass the self-owned-source gate (`void Wire(UiElement AssociatedObject) { AssociatedObject.Loaded += H; }`). That source is not co-lifetimed with the behavior, so the subscription must stay flagged. The name match now consults the symbol: the genuine base accessor is either UNRESOLVED (null — the Interactivity assembly is absent on the runner, the normal WPF case) or an INHERITED member (containing type is a base, not this class); a local/parameter binding, or a member declared on this class, is a shadow and denies the exemption. Conservative — an unresolvable symbol keeps today's behaviour. New flagged control ShadowParamBehavior (a parameter named AssociatedObject) + a CI assertion. Verified with the real extractor: all five controls warn, the three positives stay silent; full-sample-set output still byte-identical to main. Refs #227 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U2AWb9N2VUsby2XpdftBcu
1 parent aa1af42 commit e3ec4fc

3 files changed

Lines changed: 48 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,11 @@ jobs:
909909
# source before the `+=` — the stale declaration binding must not exempt.
910910
echo "$out" | grep -qE "AssociatedObjectSourceSample\.cs:[0-9]+: warning: \[OWN001\].*'ReassignedLocalBehavior'" \
911911
|| { echo "FAIL: expected OWN001 when the AssociatedObject-bound local is reassigned before the +="; exit 1; }
912+
# (4, Codex P2) a PARAMETER named `AssociatedObject` SHADOWS the inherited base
913+
# accessor — the name matches by text, but the symbol is an injected parameter,
914+
# so the exemption must resolve the binding, not just the name.
915+
echo "$out" | grep -qE "AssociatedObjectSourceSample\.cs:[0-9]+: warning: \[OWN001\].*'ShadowParamBehavior'" \
916+
|| { echo "FAIL: expected OWN001 when a shadowing parameter is named AssociatedObject (#227)"; exit 1; }
912917
echo "OK: real C# -> facts -> OWN001 (subscription + timer + field + Subscribe + pool + local) + OWN014 (static-event region escape) + DI001 (captive dependency) + DI002 (scoped captured weakly) + DI003 (transient IDisposable captured by a singleton) + DI004 (transient IDisposable service-located from the root provider) + DI005 (scoped service cached from a created scope) + [OwnIgnore] suppression (silent-but-counted, SARIF suppressions) + #218 DP old->new subscription rotation (silent; controls flagged) + #225 empty-Dispose local exemption (silent; controls flagged) + #228 curated app-scoped source in App (silent; controls flagged) + #227 self-owned Behavior.AssociatedObject source (silent; controls flagged) at the C# location"
913918
- name: Flow-sensitive local IDisposables (--flow-locals, P-016 B0b/B2)
914919
run: |

frontend/roslyn/OwnSharp.Extractor/Program.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,20 +1040,42 @@ static bool IsBehaviorSubscriber(TypeDeclarationSyntax cls)
10401040
};
10411041

10421042
// #227: a bare/`this`-qualified read of the base-class `AssociatedObject` accessor —
1043-
// `this.AssociatedObject` or `AssociatedObject`. Matched by NAME (the property is
1044-
// inherited from the unresolved `Behavior` base), through casts/`!`. A member access
1045-
// qualified with anything else (`other.AssociatedObject`) reaches ANOTHER object and
1046-
// is not this behavior's own attached element, so it is rejected.
1047-
static bool IsAssociatedObjectAccess(ExpressionSyntax expr)
1043+
// `this.AssociatedObject` or `AssociatedObject`. The NAME is matched syntactically (the
1044+
// property is inherited from the `Behavior` base, which does not resolve on the Linux
1045+
// runner), through casts/`!`. A member access qualified with anything else
1046+
// (`other.AssociatedObject`) reaches ANOTHER object and is rejected.
1047+
//
1048+
// But the name alone is not enough: a local, a parameter, or a hidden member DECLARED in
1049+
// this class named `AssociatedObject` can SHADOW the inherited accessor and hold an
1050+
// injected publisher (`void Wire(UiElement AssociatedObject) { AssociatedObject.Loaded +=
1051+
// H; }`) — that source is NOT co-lifetimed with the behavior (Codex P2). So the symbol is
1052+
// checked: the genuine base accessor is either UNRESOLVED (null — the Interactivity
1053+
// assembly is absent, the normal WPF case) or an INHERITED member (containing type is a
1054+
// base, not this class); a local/parameter binding, or a member declared on this class,
1055+
// is a shadow and denies the exemption.
1056+
static bool IsAssociatedObjectAccess(ExpressionSyntax expr, SemanticModel model,
1057+
TypeDeclarationSyntax clsNode)
10481058
{
10491059
expr = StripCasts(expr);
1050-
return expr switch
1060+
var nameMatches = expr switch
10511061
{
10521062
MemberAccessExpressionSyntax m => m.Name.Identifier.Text == "AssociatedObject"
10531063
&& m.Expression is ThisExpressionSyntax,
10541064
IdentifierNameSyntax id => id.Identifier.Text == "AssociatedObject",
10551065
_ => false,
10561066
};
1067+
if (!nameMatches)
1068+
return false;
1069+
var sym = model.GetSymbolInfo(expr).Symbol;
1070+
if (sym is null)
1071+
return true; // unresolved inherited accessor (the WPF runner case)
1072+
if (sym is ILocalSymbol or IParameterSymbol)
1073+
return false; // a shadowing local/parameter, not the accessor
1074+
if (sym is IFieldSymbol or IPropertySymbol
1075+
&& model.GetDeclaredSymbol(clsNode) is { } clsSym
1076+
&& SymbolEqualityComparer.Default.Equals(sym.ContainingType, clsSym))
1077+
return false; // a hidden own member shadowing the accessor
1078+
return true; // an inherited (or resolvable base) accessor
10571079
}
10581080

10591081
// #227: does `expr` provably resolve to `this.AssociatedObject` — directly, or through
@@ -1070,7 +1092,7 @@ static bool ResolvesToAssociatedObject(ExpressionSyntax expr, SemanticModel mode
10701092
if (depth > 4)
10711093
return false;
10721094
expr = StripCasts(expr);
1073-
if (IsAssociatedObjectAccess(expr))
1095+
if (IsAssociatedObjectAccess(expr, model, clsNode))
10741096
return true;
10751097
var sym = model.GetSymbolInfo(expr).Symbol;
10761098
if (sym is ILocalSymbol local)

frontend/roslyn/samples/AssociatedObjectSourceSample.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,18 @@ protected void OnAttached()
141141

142142
void OnLoaded(object? sender, EventArgs e) { }
143143
}
144+
145+
// CONTROL 4 (flagged, Codex P2): a PARAMETER named `AssociatedObject` SHADOWS the
146+
// inherited base accessor — the identifier text matches, but the symbol is an
147+
// injected parameter, not the co-lifetimed attached element, so the exemption must
148+
// check the binding, not just the name.
149+
public class ShadowParamBehavior : Behavior<UiElement>
150+
{
151+
public void Wire(UiElement AssociatedObject)
152+
{
153+
AssociatedObject.Loaded += OnLoaded; // OWN001: shadowing parameter
154+
}
155+
156+
void OnLoaded(object? sender, EventArgs e) { }
157+
}
144158
}

0 commit comments

Comments
 (0)