Skip to content

feat(di): anchor DI004 at the GetRequiredService call site (P-006 Q#1, DI004 arm)#67

Merged
PhysShell merged 3 commits into
mainfrom
claude/di004-callsite-anchor
Jun 21, 2026
Merged

feat(di): anchor DI004 at the GetRequiredService call site (P-006 Q#1, DI004 arm)#67
PhysShell merged 3 commits into
mainfrom
claude/di004-callsite-anchor

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Anchor DI004 at its real consumer — the resolution call site

#66 anchored DI001/DI002/DI003 at the consuming constructor. DI004's consumer is different: a resolution call site (GetRequiredService<T>()), not a ctor. This arm finishes the anchoring story — now all four DI rules point at their consumer.

DiCaptiveSample.cs:79: warning: [DI004] singleton 'ConnectionResolver' resolves transient
IDisposable 'PooledConnection' by hand … (ConnectionResolver -> PooledConnection)
  [singleton registered at DiCaptiveSample.cs:192]
        ↑ PRIMARY anchor = the GetRequiredService call site (line 79); registration is secondary

The call site is the PRIMARY anchor (the finding's file/line), not just a related location — because unlike DI001/2/3 (a registration-graph property, rightly anchored at the registration, where you'd change the lifetime), DI004 is a call-site property: the leak is _sp.GetRequiredService<T>() and the fix is there too, while the resolver's registration is barely relevant. The registration is demoted to the secondary: a Finding.related location + a [singleton registered at …] message tail.

(This corrects the first cut, which left the call site as related-only and kept the registration as primary — Codex caught that human/GitHub/MSBuild still annotated Startup.cs, not the leak.)

The transitive subtlety

The site is the entry type's call (path[1]), not the leaked disposable. For WrapperResolver -> MidConnection -> PooledConnection, the anchor points at where MidConnection was hand-resolved (line 137) — that's the service-location call the developer wrote; PooledConnection was dragged in by the container.

How

  • Extractor: RootResolvedTypes yields the call line too; the service fact gains a parallel root_resolve_sites ({type, file, line}), alongside the existing string-array root_resolves (additive, backward-compatible).
  • Core (ownlang/di.py): ExplicitRootResolution gains resolved_file/resolved_line (the entry call site); the bridge makes that the finding's primary location, the registration its related, with a clean fallback to the registration when the call is unknown. ownir.py validates root_resolve_sites in load().

Pinned

layer what
wpf-extractor CI primary line = call site: ConnectionResolver:79, ExprBodiedResolver:123, transitive WrapperResolver:137; registration named as the secondary
tests/test_ownir.py direct + transitive call site, the call-site-primary Finding, the registration related, root_resolve_sites load-validation

Validated locally: ruff + mypy --strict clean, ownir 116/116; DI001=4 / DI002=3 / DI003=1 / DI004=3 counts unchanged.

Next anchoring follow-up (separate slice): per-parameter precision for the captive anchor.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED

Summary by CodeRabbit

  • Improvements
    • Enhanced DI004 diagnostics to anchor findings to the service resolution call site (GetRequiredService/GetService), including direct and transitive cases.
    • Registration-site details are now provided as a secondary/related location for better navigation (with improved location reporting).
  • Documentation
    • Updated DI004 guidance and the proposal to match the resolution-call-site anchoring behavior and new call-site metadata.
  • Tests
    • Expanded DI004 bridge coverage for explicit root resolutions and added schema-validation to reject malformed call-site metadata.
  • CI
    • Strengthened DI004 CI assertions to verify exact call-site anchoring and registration-site suffix presence.

…, DI004 arm)

#66 anchored DI001/2/3 at the consuming constructor; DI004's consumer is different — a
resolution CALL SITE (`GetRequiredService<T>()`), not a ctor. The extractor now emits, in
parallel with `root_resolves`, a `root_resolve_sites` fact ({type, file, line} per resolved
type — the call's location); the core appends ` [resolved at <file>:<line>]` to the DI004
message and carries the site as a structured Finding.related triple -> SARIF relatedLocation,
same shape as #66.

The site is the ENTRY type's call (path[1]): for a transitive leak (WrapperResolver ->
MidConnection -> PooledConnection) it points at where MidConnection was hand-resolved, not at
the container-built PooledConnection. Degrades cleanly (no suffix, no related) when absent.
ownir load() validates root_resolve_sites as an array of {type:str, file:str, line:int}.

Pinned end-to-end by DiCaptiveSample.cs (ConnectionResolver:79, ExprBodiedResolver:123,
transitive WrapperResolver:137) in the wpf-extractor CI job, and at the graph level (direct +
transitive call site, related location, load validation) by tests/test_ownir.py. ruff + mypy
--strict clean, ownir 116/116; DI counts unchanged. Now all four DI rules anchor at their
consumer. Remaining anchoring follow-up: per-parameter precision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 19cfcfb2-b106-42a0-ab13-af9b59a5cb76

📥 Commits

Reviewing files that changed from the base of the PR and between e11ef62 and 32c1fe1.

📒 Files selected for processing (1)
  • .github/workflows/ci.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yml

📝 Walkthrough

Walkthrough

DI004 findings are now anchored at GetService<T>/GetRequiredService<T> call sites. The Roslyn extractor emits a new root_resolve_sites fact (type, file, line per resolution). OwnIR validates and parses this data; the DI core model stores it on Service and ExplicitRootResolution; the DI004 finding attaches a relatedLocation and message suffix pointing to the call site.

Changes

DI004 Resolution Call-Site Anchoring

Layer / File(s) Summary
Roslyn extractor: RootResolvedTypes and root_resolve_sites emission
frontend/roslyn/OwnSharp.Extractor/Program.cs
RootResolvedTypes now yields (type, line) tuples; ExtractServices accumulates a classRootResolveSites map and emits a new root_resolve_sites field in each service fact JSON.
DI core model: Service and ExplicitRootResolution call-site fields
ownlang/di.py
Service gains a root_resolve_sites field of (type, file, line) tuples; ExplicitRootResolution gains resolved_file/resolved_line fields and a conditional message suffix; find_explicit_root_resolutions builds a sites lookup and populates those fields on each emitted finding.
OwnIR schema validation, helpers, and DI004 wiring
ownlang/ownir.py
load() validates the optional root_resolve_sites array shape with strict type checks; new _di004_primary, _di004_related, and _resolve_sites helpers parse and structure call-site data; _di_findings populates Service.root_resolve_sites and uses these helpers to anchor DI004 findings at the call site with registration as a related location.
Tests, CI assertions, and docs
tests/test_ownir.py, .github/workflows/ci.yml, docs/notes/di-captive-extractor.md, docs/proposals/P-006-di-lifetimes.md
Test fixtures add root_resolve_sites metadata and assertions validate call-site anchors in messages and related tuples; a negative test guards malformed line values; CI adds regex checks for singleton registered at suffixes; docs updated to describe the implemented call-site anchoring behavior and resolve the prior design decision on DI004 anchoring.

Sequence Diagram(s)

sequenceDiagram
  participant Extractor as OwnSharp.Extractor
  participant OwnIR as ownlang/ownir.py
  participant DI as ownlang/di.py

  Extractor->>Extractor: RootResolvedTypes yields (type, line) per GetService call
  Extractor->>OwnIR: emit service fact with root_resolve_sites [{type, file, line}]
  OwnIR->>OwnIR: load() validates root_resolve_sites shape
  OwnIR->>OwnIR: _resolve_sites() parses to (type, file, line) triples
  OwnIR->>DI: construct Service with root_resolve_sites
  DI->>DI: find_explicit_root_resolutions builds sites lookup
  DI->>DI: ExplicitRootResolution populated with resolved_file, resolved_line
  OwnIR->>OwnIR: _di004_primary/_di004_related anchor Finding at call site
  OwnIR-->>OwnIR: Finding emitted with call-site location and registration-related tuple
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PhysShell/Own.NET#49: Both PRs modify the Roslyn extractor's ExtractServices and the emitted services fact schema—feat(extractor): DI registration graph → DI001 captive dependency on real C# (P-006) #49 adds the base services facts from DI registrations for DI001, and this PR extends that same pipeline to also emit root_resolve_sites call-site metadata used to anchor DI004 findings.
  • PhysShell/Own.NET#65: Introduced the core DI004 root_resolves/find_explicit_root_resolutions pipeline that this PR extends with per-resolution call-site metadata (root_resolve_sites) for precise call-site anchoring.
  • PhysShell/Own.NET#66: The main PR's DI004 implementation uses the existing Finding.related/SARIF relatedLocations mechanism (via _di004_related) introduced in that PR for anchoring consuming constructors in DI001/DI002/DI003.

Poem

🐇 Hop, hop — the service was found,
at line forty-two, underground!
GetRequiredService called with care,
the call site anchored, beyond compare.
DI004 now knows just where you've been —
resolved at the source, not the in-between! 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: anchoring DI004 at GetRequiredService call site, with specific reference to the design decision (P-006 Q#1) and affected component (DI004 arm).
Linked Issues check ✅ Passed The PR implements SARIF relatedLocation support for DI004 diagnostics [#39], enabling structured diagnostic output and clickable navigation in code scanning tools as advocated in the linked issue.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing DI004 call-site anchoring with SARIF support across the extractor, core framework, and tests; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/di004-callsite-anchor

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)

380-386: ⚡ Quick win

Add the missing ExprBodiedResolver call-site assertion.

Line 381 states three DI004 call-site anchors are pinned, but the checks at Line 383–386 only assert two. A regression on ExprBodiedResolver:123 would currently pass CI.

Suggested CI assertion
           echo "$out" | grep -qE "'ConnectionResolver'.*resolved at .*DiCaptiveSample\.cs:79\]" \
             || { echo "FAIL: expected the DI004 call-site anchor (ConnectionResolver, line 79)"; exit 1; }
+          echo "$out" | grep -qE "'ExprBodiedResolver'.*resolved at .*DiCaptiveSample\.cs:123\]" \
+            || { echo "FAIL: expected the DI004 call-site anchor (ExprBodiedResolver, line 123)"; exit 1; }
           echo "$out" | grep -qE "'WrapperResolver'.*resolved at .*DiCaptiveSample\.cs:137\]" \
             || { echo "FAIL: expected the transitive DI004 call-site anchor (WrapperResolver entry, line 137)"; exit 1; }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 380 - 386, The CI workflow is missing
an assertion for the ExprBodiedResolver call-site anchor mentioned in the
comment at line 381. Add a grep check after the existing ConnectionResolver
assertion that validates ExprBodiedResolver is resolved at line 123 in
DiCaptiveSample.cs, following the same pattern as the ConnectionResolver and
WrapperResolver checks above it. This ensures that any regression in the
ExprBodiedResolver DI004 anchor would be caught by CI.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 380-386: The CI workflow is missing an assertion for the
ExprBodiedResolver call-site anchor mentioned in the comment at line 381. Add a
grep check after the existing ConnectionResolver assertion that validates
ExprBodiedResolver is resolved at line 123 in DiCaptiveSample.cs, following the
same pattern as the ConnectionResolver and WrapperResolver checks above it. This
ensures that any regression in the ExprBodiedResolver DI004 anchor would be
caught by CI.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 029bc2ae-e828-43c4-844c-297ee4e5c28a

📥 Commits

Reviewing files that changed from the base of the PR and between 1ad65ef and ae6595e.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • docs/notes/di-captive-extractor.md
  • docs/proposals/P-006-di-lifetimes.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • ownlang/di.py
  • ownlang/ownir.py
  • tests/test_ownir.py

@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: ae6595e136

ℹ️ 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".

Comment thread ownlang/ownir.py Outdated
component=c.singleton, event=c.resolved, handler="",
message=c.message, kind="DI lifetime", severity="warning")
message=c.message, kind="DI lifetime", severity="warning",
related=_resolution_related(c))

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 Use the DI004 call site as the primary location

Adding the GetRequiredService site only as related leaves the actual finding anchored at c.file/c.line above, which are still the service registration site. In the new DI004 path this means human/GitHub/MSBuild output continues to annotate Startup.cs rather than the leaking _sp.GetRequiredService<T>() invocation, and SARIF still has the registration as its primary location with the call as secondary. For facts like file: S.cs,line:5 plus root_resolve_sites: R.cs:42, check_facts() still returns S.cs:5, so the advertised call-site anchoring is not achieved.

Useful? React with 👍 / 👎.

…d (Codex)

Codex P2: the prior commit added the GetRequiredService call site only as a `related`
location, so the DI004 finding stayed anchored at the registration site (Startup.cs) —
human/GitHub/MSBuild/SARIF still annotated the registration, not the leaking call. The
PR's stated goal (anchor at the call site) was not actually delivered.

Unlike DI001/2/3 (a registration-graph property, rightly anchored at the registration),
DI004 is a CALL-SITE property — the leak *is* `_sp.GetRequiredService<T>()` and the fix is
there too, while the resolver's registration is barely relevant. So flip it: the call site
is DI004's PRIMARY anchor (the finding's file/line), with the registration demoted to the
Finding.related secondary + a `[singleton registered at <reg>]` message tail. Falls back to
the registration site when the call is unknown.

Also add the missing ExprBodiedResolver:123 call-site CI assertion (CodeRabbit: the comment
claimed three anchors, only two were checked). ruff + mypy --strict clean, ownir 116/116;
DI counts unchanged. CI now pins the primary line = call site (79/123/137) + the registration
named as the secondary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)

391-392: 💤 Low value

Consider counting "singleton registered at" occurrences for precision.

The current assertion checks for the presence of "singleton registered at" anywhere in the output, which validates that the message suffix is being generated. For slightly better precision, consider counting the occurrences to ensure it appears exactly 3 times (once for each DI004 finding), mirroring the existing pattern at lines 308 and 377:

-          echo "$out" | grep -q "singleton registered at " \
-            || { echo "FAIL: expected the DI004 registration site named as the secondary anchor"; exit 1; }
+          nreg=$(echo "$out" | grep -c "singleton registered at ")
+          [ "$nreg" = "3" ] \
+            || { echo "FAIL: expected exactly 3 DI004 findings with registration-site suffix, got $nreg"; exit 1; }

This would catch regressions where the suffix is generated for some findings but not all.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 391 - 392, The grep assertion at lines
391-392 only checks for the presence of "singleton registered at" in the output,
but it should verify that this string appears exactly 3 times (once for each
DI004 finding) to catch regressions where the suffix is only generated for some
findings but not all. Replace the grep -q presence check with a count-based
assertion that verifies the occurrence count equals 3, following the same
pattern already used at lines 308 and 377 in the workflow file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 391-392: The grep assertion at lines 391-392 only checks for the
presence of "singleton registered at" in the output, but it should verify that
this string appears exactly 3 times (once for each DI004 finding) to catch
regressions where the suffix is only generated for some findings but not all.
Replace the grep -q presence check with a count-based assertion that verifies
the occurrence count equals 3, following the same pattern already used at lines
308 and 377 in the workflow file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 05393b62-6cad-4585-a869-69c6f5d747cb

📥 Commits

Reviewing files that changed from the base of the PR and between ae6595e and e11ef62.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • docs/notes/di-captive-extractor.md
  • docs/proposals/P-006-di-lifetimes.md
  • ownlang/di.py
  • ownlang/ownir.py
  • tests/test_ownir.py
✅ Files skipped from review due to trivial changes (1)
  • docs/notes/di-captive-extractor.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • ownlang/di.py
  • tests/test_ownir.py

…presence

CodeRabbit review: the registration-site secondary-anchor assertion used grep -q
(presence anywhere), which would pass if the `[singleton registered at …]` tail were
generated for some DI004 findings but not all. Count it (== 3, one per finding),
mirroring the existing exactly-N patterns. CI-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@PhysShell
PhysShell merged commit 53950f8 into main Jun 21, 2026
22 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.

2 participants