feat(dagger): bridge @Provides / @Binds interface→impl bindings#416
Closed
arttttt wants to merge 4 commits into
Closed
feat(dagger): bridge @Provides / @Binds interface→impl bindings#416arttttt wants to merge 4 commits into
arttttt wants to merge 4 commits into
Conversation
Adds a minimal `daggerResolver` to the framework registry — `detect()` scans .java/.kt files for `import dagger.*` so `detectFrameworks` can report Dagger usage to downstream consumers. Binding edge synthesis lives in the callback-synthesizer (whole-graph pass needed to disambiguate across modules); the resolver is the architectural counterpart that gives Dagger a name in the registry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `daggerProvidesBindingEdges` to the whole-graph synthesizer pass.
For each `@Module` class in a file that actually imports `dagger.*`,
scans its methods for the binding shape — `@Binds abstract Interface
m(Impl impl)` (always pure) or `@Provides Interface m(Impl impl)
{ return impl; }` (identity-body verified to filter factories).
Emits `interface → impl` `references` edges tagged with
`synthesizedBy: 'dagger-provides'`, keyed so dupes across modules
collapse cleanly. Java + Kotlin via per-language head parsers; source
scanning is required because JVM annotations aren't surfaced as a
`decorators` field by the extractor.
Conservative on purpose — body-identity check filters Plaid-style
factory methods (`@Provides X provideX(Dep dep) { return Factory.of(dep) }`)
that share the Interface(Impl) signature but are NOT bindings. Loss:
real-world projects whose bindings live in factory bodies (rare in
production Dagger code) are missed; gain: zero false positives on
projects like Plaid where Dagger is used mostly for factories +
Android SDK type bindings.
Validated on real repos:
- janishar/android-mvp-architecture (98 .java, classic Module pattern):
12/12 in-project bindings detected (DataManager → AppDataManager,
DbHelper → AppDbHelper, 8 presenter bindings, …).
- nickbutcher/plaid (322 JVM, multi-module pure Dagger 2):
0 bindings — correct, all @provides are factories and all @BINDS
target Android SDK classes not present in the graph.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seven cases covering the feature contract: - @provides Interface(Impl) with return-impl body — emitted. - @BINDS abstract method — emitted (no body check needed). - Kotlin @module class with @provides fn(impl): Iface = impl — emitted. - Two impls of the same interface across separate modules — both bindings carried (each is binding-precise). - @provides without an impl parameter — NOT emitted. - @provides Iface(Impl) whose body builds via factory — NOT emitted (the Plaid-style failure mode). - Identical signature outside an @module class — NOT emitted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
interface → implreferencesedges for Dagger 2 / Hilt module bindings (Java + Kotlin).@Bindsis taken unconditionally (abstract — the declaration IS the binding);@Providesrequires a pure-identity body (return impl;/= impl) so factory methods that happen to share theInterface(Impl)signature shape don't get claimed.daggerResolverin the framework registry —detect()scans.java/.ktfiles forimport dagger.*sodetectFrameworkscan report Dagger usage.Why
Most Android architectures terminate at a Dagger binding:
Activity → ViewModel → SomeRepositoryinjectsSomeRepository, and the runtime swap toSomeRepositoryImpllives in a@Provides/@Bindsdeclaration with no static edge to it. The genericinterface-implsynthesizer already coversclass X implements Y, but a Dagger project that uses@Moduleto wire bindings across modules can have several impls per interface —interface-implwould either link to all of them (noise) or be capped out. Reading the binding declarations gives a binding-precise edge from the runtime contract.How
callback-synthesizer.tsgetsdaggerProvidesBindingEdges(). For eachclassnode:dagger.*(filters Guice / NestJS / custom@Moduleannotations).@Module.methodnode, read a 7-line window around the signature, check it has@Providesor@Binds, parse(returnType, paramType, paramName)from the head with per-language regexes (Kotlin signatures aren't populated by the extractor — grammar lacks field names).@Provides, additionally require the body to literallyreturn paramName(Java) or= paramName(Kotlin).@Bindsskips the body check (abstract).interface → impl,kind: 'references',provenance: 'heuristic',metadata: { synthesizedBy: 'dagger-provides', via, registeredAt }.frameworks/dagger.tsis a slim resolver (detectonly — no per-ref resolution). Lives in the registry so other consumers can flag Dagger projects.Validation on real repos
janishar/android-mvp-architecturegrep '@Provides'ground truth.nickbutcher/plaid@Providesare all factories (e.g.provideMarkdown(): Markdown = Bypass(...),provideOkHttpClient(...): OkHttpClient = OkHttpClient.Builder()...), and its@Bindstargets are Android SDK types not in the indexed graph (HomeActivity → FragmentActivityetc.). The strict body-identity check correctly avoids claiming the factories as bindings.The Plaid result is the more important one — it pins the precision side of the contract.
Tests
__tests__/frameworks-integration.test.ts— new describeDagger 2 — @Provides / @Binds binding synthesis. Seven cases:@Provides Iface(Impl) { return impl; }→ emitted@Binds abstract Iface(Impl)→ emitted@Module class { @Provides fn(impl: Impl): Iface = impl }→ emitted@Provides Iface(Impl) { return factory.create(); }→ NOT emitted (Plaid pattern)@Provides X provideX()(no impl param) → NOT emitted@Moduleclass → NOT emittedAll 123 tests in the integration + resolution + frameworks + full-pipeline suites stay green.
Scope notes
@Module+@Provides/@Bindsshapes — it should work transparently. Hilt-specific annotations (@HiltAndroidApp,@AndroidEntryPoint,@HiltViewModel) aren't required for binding edges and are left for a follow-up.@IntoSet/@IntoMap/ qualifiers (@Named) /@AssistedInjectare deferred.@Component(modules = [FooModule.class])chain isn't emitted as an explicit edge — Component → binding is reachable via the existingcontainsandreferencesedges.@Provideswith factory body is intentionally not bridged — see the precision/recall trade-off in the Plaid validation above.How I got here
Built off
main(not stacked on #412 or #413 — Dagger is independent). Discovered during validation on Plaid that the first cut over-matched factory methods sharing the binding signature shape; tightened the body-identity check, re-validated against both the small Dagger sample and Plaid before opening the PR.