Skip to content

feat(di): anchor captive findings at the consuming constructor (P-006 Q#1)#66

Merged
PhysShell merged 4 commits into
mainfrom
claude/di-consuming-constructor
Jun 21, 2026
Merged

feat(di): anchor captive findings at the consuming constructor (P-006 Q#1)#66
PhysShell merged 4 commits into
mainfrom
claude/di-consuming-constructor

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Anchor the capture at the code, not just the wiring

A captive finding's primary anchor is the registration site — where you wire the lifetime. But the capture is introduced elsewhere: the consuming constructor that injects the captive dependency. P-006 open question #1"anchor at the registration line, the consuming constructor, or both?" — is now answered both, with the path shown (like OWN014).

DI001 / DI002 / DI003 now also name the consuming constructor:

DiCaptiveSample.cs:78: warning: [DI001] singleton 'EmailSender' captures scoped service
'AppDbContext' (captive dependency: EmailSender -> AppDbContext)
  [consumed by the 'EmailSender' constructor at DiCaptiveSample.cs:25]  ← new
  • in the message tail (visible on every surface — human, GitHub annotation, MSBuild, SARIF), and
  • as a structured SARIF relatedLocation — which GitHub code scanning renders as a second, clickable, labelled location, and which is cross-file (the registration may live in Startup.cs, the ctor in EmailSender.cs).

The primary anchor stays at the registration site (one fix site; no churn to the line-based assertions), so this is purely additive.

How

  • Extractor: records each implementation's ctor location — the widest public constructor's declaration, or the class declaration for a C# 12 primary / implicit ctor — into ctor_file / ctor_line on the service fact (classCtorLoc).
  • Core (ownlang/di.py): the three captive dataclasses gain consumed_file/consumed_line; _consumed_suffix appends the tail when known. ownir.py adds a Finding.related triple list → SARIF relatedLocations, set for DI001/2/3 from the singleton's ctor. load() validates ctor_file/ctor_line like file/line.
  • Degrades cleanly: unknown ctor location → no tail, no related location, so hand-authored facts and an older extractor still produce the registration-anchored form.

Pinned

layer what
wpf-extractor CI explicit-ctor EmailSender:25, primary-ctor PrimaryCtorService:33, ConnectionWarmer:50 (DI003), WeakCache:57 (DI002)
di.facts.json fixture the cross-file case (registration Startup.cs, ctor EmailSender.cs:5)
tests/test_ownir.py the message tail, the structured related triple, the SARIF relatedLocation, and clean degradation when the ctor location is absent

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

Scope

This covers the ctor-injected captive family (DI001/2/3). DI004's consumer is a resolution call site (GetRequiredService<T>()), not a constructor — anchoring it there needs the call-site location threaded through root_resolves, and is the natural sibling follow-up (noted in the docs).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Captive-dependency diagnostics (DI001, DI002, DI003) now include a consuming-constructor “consumed by …” location and expose it via structured SARIF relatedLocations (cross-file clickable).
  • Documentation

    • Updated P-006 docs to explain consuming-constructor anchoring and note that DI004/call-site anchoring is deferred.
  • Bug Fixes / Improvements

    • OwnIR import/export now carries consuming-constructor metadata (ctor_file, ctor_line, ctor_type) and uses it to populate the related location anchors (with graceful degradation when unknown).
  • Tests / CI

    • Expanded DI001/DI003 assertions, updated fixtures, added validation/regression checks, and extended CI verification for P-006.

… Q#1)

DI001/DI002/DI003 keep their primary anchor at the registration site but now also
name the CONSUMING CONSTRUCTOR — where the captive dependency is injected — so the
developer is pointed at the code, not just the wiring. P-006 open question #1 ("anchor
at the registration line, the consuming constructor, or both?") answered: both, with
the capture path shown, like OWN014.

- Extractor records each implementation's ctor location (the widest public ctor's
  declaration, or the class declaration for a C# 12 primary / implicit ctor) into
  ctor_file / ctor_line on the service fact.
- Core appends ` [consumed by the '<singleton>' constructor at <file>:<line>]` to the
  captive message (every surface) and carries it as a structured Finding.related triple,
  emitted as a SARIF relatedLocation — clickable + cross-file (registration in Startup.cs,
  ctor in EmailSender.cs). Degrades cleanly (no tail, no related) when the location is
  unknown, so hand-authored facts / an older extractor still produce the prior form.
- ownir load() validates ctor_file/ctor_line like file/line.

Pinned end-to-end by DiCaptiveSample.cs (explicit-ctor EmailSender:25, primary-ctor
PrimaryCtorService:33, ConnectionWarmer:50, WeakCache:57) in the wpf-extractor CI job, the
cross-file case by the di.facts.json fixture, and the SARIF related location by
tests/test_ownir.py. DI004's consumer is a resolution call site (not a ctor) — its anchor
is the sibling follow-up. ruff + mypy --strict clean, ownir 109/109; DI counts unchanged.

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: 9d0e806d-d75b-493d-913f-de9cdf7035dc

📥 Commits

Reviewing files that changed from the base of the PR and between 3b3ecea and cc2d862.

📒 Files selected for processing (2)
  • ownlang/di.py
  • tests/test_ownir.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • ownlang/di.py

📝 Walkthrough

Walkthrough

Adds consuming-constructor location anchors to DI001/DI002/DI003 captive-dependency findings. The Roslyn extractor records each class's constructor file/line, propagates those through DI dataclasses and message text in ownlang/di.py, and surfaces them as Finding.related and SARIF relatedLocations in ownlang/ownir.py. Tests, fixtures, CI assertions, and docs are updated accordingly.

Changes

Consuming-Constructor Anchor Pipeline

Layer / File(s) Summary
Roslyn extractor: capture ctor location in service facts
frontend/roslyn/OwnSharp.Extractor/Program.cs
Builds a classCtorLoc map recording the file and 1-based line of each class's widest public constructor (or class declaration for primary/implicit ctors), then emits ctor_file, ctor_line, and ctor_type in each service registration fact.
DI dataclasses: consumed_file/consumed_line fields and _consumed_suffix helper
ownlang/di.py
Introduces _consumed_suffix() helper; extends Service, CaptiveDependency, WeakCaptiveDependency, and CapturedTransientDisposable with consumed_file/consumed_line/consumed_type fields; updates message properties to append the suffix; and updates all three find_*() collectors to populate the fields from Service.ctor_file/ctor_line/ctor_type.
OwnIR: Finding.related, SARIF relatedLocations, and DI wiring
ownlang/ownir.py
Adds related field to Finding for secondary location tuples; updates _sarif_result() to emit SARIF relatedLocations; extends load() to validate ctor_file/ctor_line/ctor_type; introduces _consumer_related() helper; wires DI001, DI002, and DI003 findings with related=_consumer_related(c).
Tests, fixture, and CI assertions
tests/fixtures/ownir/di.facts.json, tests/test_ownir.py, .github/workflows/ci.yml
Updates fixture with ctor_file/ctor_line/ctor_type on EmailSender and ReportService; adds DI001 assertions for consuming-constructor anchor in message/related/SARIF, unknown-location degradation, interface-based registration, and type validation; extends DI003 test setup and assertions; extends CI grep checks for DI001/DI002/DI003 consuming-constructor strings.
Docs: consuming-constructor anchor design and proposal update
docs/notes/di-captive-extractor.md, docs/proposals/P-006-di-lifetimes.md
Documents the consuming-constructor anchor behavior (message tail, SARIF relatedLocation, degradation, next slices) and marks the DI001–DI003 anchoring open question as resolved in the P-006 proposal, noting DI004 call-site anchoring as still open.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PhysShell/Own.NET#49: Modifies the same ExtractServices / services fact emission in frontend/roslyn/OwnSharp.Extractor/Program.cs for DI001 captive-dependency findings.
  • PhysShell/Own.NET#56: Introduces the DI003 diagnostic end-to-end, including CapturedTransientDisposable and DI003 emission in ownlang/ownir.py, which this PR extends with consuming-constructor anchors.
  • PhysShell/Own.NET#63: Modifies the same DI002 WeakCaptiveDependency pipeline in ownlang/di.py and ownlang/ownir.py that this PR extends with consuming-constructor relatedLocation anchoring.

Poem

🐇 Hop, hop, I've found the ctor's lair,
At file and line I'll plant my anchor there!
DI001, DI002, DI003 in a row —
"Consumed by the constructor," now all findings know.
No more phantom singletons lost in the stack,
The rabbit traced the path and hopped right back! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding consuming constructor anchoring to captive dependency findings (DI001/DI002/DI003) as part of P-006.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/di-consuming-constructor

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

…ertions

The extractor emits the ctor location with the full relative path
(frontend/roslyn/samples/DiCaptiveSample.cs:25), but the new DI consuming-constructor
assertions grepped for the bare filename (at DiCaptiveSample.cs:25), which is not a
substring — so the wpf-extractor job failed even though the output was correct. Switch
the four assertions to a regex that allows the path prefix (`at .*DiCaptiveSample\.cs:NN]`),
matching how the existing DI greps already tolerate it. Feature unchanged; CI-only.

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

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

ℹ️ 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/di.py Outdated
message degrades cleanly to just the registration-anchored form."""
if line < 1:
return ""
return f" [consumed by the '{singleton}' constructor at {file}:{line}]"

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 implementation names for constructor anchors

For registrations where service and implementation types differ, e.g. AddSingleton<IEmailSender, EmailSender>(), the extractor stores the implementation constructor location but the new suffix formats singleton (the service name) as the constructor owner. That makes diagnostics/SARIF say the IEmailSender constructor is at EmailSender.cs, which points users to a non-existent constructor; carry the implementation/ctor type through the fact or avoid naming the constructor owner.

Useful? React with 👍 / 👎.

@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)
docs/proposals/P-006-di-lifetimes.md (1)

114-115: 💤 Low value

Minor: Consider splitting the resolved answer for readability.

The sentence combining the question, its resolution, and the explanation is dense. A brief split (e.g., "Resolved — both. For the captive family (DI001/DI002/DI003)…") would improve scannability in this technical proposal.

🤖 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 `@docs/proposals/P-006-di-lifetimes.md` around lines 114 - 115, Split the dense
sentence at lines 114-115 that combines the question "constructor, or both?"
with the resolution and explanation. Separate the brief resolution "Resolved —
both" from the detailed explanation about the capture path display. Restructure
it so the resolution stands on its own first, then introduce the captive family
(DI001/DI002/DI003) reference and related explanation on a new line or sentence
for improved scannability in this technical proposal document.

Source: Linters/SAST tools

🤖 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 `@docs/proposals/P-006-di-lifetimes.md`:
- Around line 114-115: Split the dense sentence at lines 114-115 that combines
the question "constructor, or both?" with the resolution and explanation.
Separate the brief resolution "Resolved — both" from the detailed explanation
about the capture path display. Restructure it so the resolution stands on its
own first, then introduce the captive family (DI001/DI002/DI003) reference and
related explanation on a new line or sentence for improved scannability in this
technical proposal document.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b5b41f21-502f-4e96-952f-99b5cee3de06

📥 Commits

Reviewing files that changed from the base of the PR and between fd3d94e and f152254.

📒 Files selected for processing (8)
  • .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/fixtures/ownir/di.facts.json
  • tests/test_ownir.py

claude added 2 commits June 21, 2026 09:54
…ner (Codex)

For an interface registration (AddSingleton<IFoo, Foo>), the singleton's service name is
the ctor-less interface IFoo, but the consuming constructor is Foo's — so naming the
service ('IFoo') as the ctor owner pointed users at a non-existent IFoo constructor. Carry
the implementation type through the fact (ctor_type = impl) and name IT in the message tail
and the SARIF related-location label. For concrete registrations ctor_type == name, so the
message is unchanged; degrades to a plain "the constructor" when the impl is unknown.

Pinned by an interface-registration case in tests/test_ownir.py (IBilling -> Billing names
Billing, never IBilling) and ctor_type load-validation. Also split the dense P-006 Q#1
sentence for readability (CodeRabbit nitpick). ruff + mypy --strict clean, ownir 112/112.

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

The Codex impl-naming fix added consumed_type to the DI001 and DI002 collectors but
missed find_captured_transient_disposables (DI003), so a DI003 finding rendered the
empty-type fallback "[consumed by the constructor at …]" instead of naming the impl
("[consumed by the 'ConnectionWarmer' constructor at …]"), failing the wpf-extractor CI
assertion. Pass consumed_type=s.ctor_type here too, and strengthen the DI003 unit test to
set ctor_type and assert the named anchor (the gap that let this through — the DI003
fixtures carried no ctor location). ruff + mypy --strict clean, ownir 113/113.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@PhysShell
PhysShell merged commit 1ad65ef into main Jun 21, 2026
22 checks passed
PhysShell added a commit that referenced this pull request Jun 21, 2026
…, DI004 arm) (#67)

#66 anchored DI001/2/3 at the consuming constructor; this finishes the anchoring story
for DI004, whose consumer is different — a resolution CALL SITE (GetRequiredService<T>()),
not a ctor. And because the leak *is* that call (the fix is there too, while the resolver's
registration is barely relevant), the call site is DI004's PRIMARY anchor — unlike the
registration-graph rules (DI001/2/3), which rightly anchor at the registration. The
registration is demoted to the Finding.related secondary + a [singleton registered at …]
message tail; falls back to the registration when the call is unknown.

- Extractor: RootResolvedTypes yields the call line; the service fact gains a parallel
  root_resolve_sites ({type, file, line}) alongside the string-array root_resolves
  (additive, backward-compatible).
- Core: ExplicitRootResolution gains resolved_file/resolved_line (the entry type's call —
  path[1], so a transitive WrapperResolver -> MidConnection -> PooledConnection anchors at
  where MidConnection was hand-resolved, not the container-built disposable); the bridge
  makes that the finding's primary location. ownir load() validates root_resolve_sites.

Pinned by DiCaptiveSample.cs (primary line = call site: ConnectionResolver:79,
ExprBodiedResolver:123, transitive WrapperResolver:137; the registration named as the
secondary, exactly 3) and by tests/test_ownir.py. ruff + mypy --strict clean, ownir 116/116;
DI001=4 / DI002=3 / DI003=1 / DI004=3 unchanged. All four DI rules now anchor at their
consumer. Next: per-parameter precision for the captive anchor.

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