feat: let a [Module] self-compile its own [Scan]#117
Conversation
A library can now keep implementations `internal` and expose only interfaces: a `[Module]` that declares a `[Scan]` compiles that scan in its own build, emitting a `public static` factory per match (which constructs the implementation and returns its accessible exposure interface) plus a single-arg `[Singleton<IExposure>(Factory = …, Fallback = Silent)]` registration into the module's partial. To a consuming container this is an ordinary module factory registration, so nothing new crosses the assembly boundary. - Add a second generator root on `[Module]`, gated on the module declaring a `[Scan]`; a plain module still emits nothing. - Expand the scan against the module's own assembly (so `internal` matches are seen), reusing the container scan's candidate discovery, filters and diagnostics. - Disposal of an internal `IDisposable` behind an interface is handled by the existing runtime disposal check for interface-returning factories. - Retire AWT154 (a module `[Scan]` is no longer rejected on import). - Add AWT194 (module with a scan not partial), AWT195 (inaccessible constructor parameter), AWT196 (no accessible exposure), AWT197 (multiple exposures) — all reported in the library's own build. v1 limitations (each reported at the library's source): a match exposes through exactly one accessible interface, and its constructor parameters must be types a consumer can name. Version-skew detection is deferred. Self-compilation is a cross-assembly feature.
Test Results 18 files ± 0 18 suites ±0 9m 27s ⏱️ + 1m 3s Results for commit 2e22e3d. ± Comparison against base commit 8c555d8. This pull request removes 4 and adds 23 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
🚀 Benchmark ResultsDetails
Details
Details
|
A [Module]'s self-compiled [Scan] now emits a [GeneratedScanRegistration] per match, which a consuming container reads as an IsScan factory registration with a synthetic per-match identity. This gives self-compiled scans container-scan semantics across the assembly boundary: - several matches under one interface collect as IEnumerable<T> instead of colliding as conflicting single-service factories (previously a consumer-side AWT111) - a match is overridable by an explicit registration (scans rank last) AWT197 still blocks a single match exposing multiple interfaces: a factory returns one type, so a singleton could not share one instance across them. Adds the generator-only GeneratedScanRegistrationAttribute<TService> to the public surface (the one new cross-assembly signal this requires).
…n module scans container-side The synthetic implementation identity of a [GeneratedScanRegistration] match was keyed by service and factory name alone, but factory names are only unique per module, so two imported modules whose matches shared a simple type name under one service collapsed into a single collection member and the later module's match was silently dropped from the collection. The identity is now qualified with the declaring module's fully-qualified name. A [Module] declared in the same compilation as its importing container never reached the container at all: a generator cannot see its own output, so the module's self-compiled [GeneratedScanRegistration] attributes are invisible within the compilation that declares the module, and the scan silently contributed nothing besides a misleading AWT151. The container now expands a same-compilation module's [Scan] directly, exactly as if it were declared on the container, with full container-scan semantics; a referenced-assembly module keeps the self-compiled path, and AWT151 counts a same-compilation module's [Scan] as a registration.
…T200, AWT201) A match carrying injection metadata the generated factory cannot mirror is now rejected as AWT200 instead of silently degrading: an [Inject] property, or a constructor parameter marked [Inject] or [Arg], carries keys, optionality, deferral or resolution-argument semantics that a mirrored plain parameter list would drop, so the consumer would construct the match differently than a container scan. A generic [Module] (or one nested in a generic type) with a [Scan] is rejected as AWT201: no closed module type exists for a consumer to import, and re-opening the declaration by its bare name silently emitted an unrelated non-generic partial carrying the registrations. Factory parameter names are emitted verbatim-prefixed (@clock), so a scanned constructor parameter legally named with a reserved keyword (@event) no longer breaks the module''s own build. Two [Scan]s on one module matching the same type under the same exposure now dedup to a single factory, mirroring the container''s per-implementation collection dedup, instead of registering two separate instances in the consumer''s collection; a differing lifetime across the overlap reports AWT142 and the first scan''s lifetime wins, as it does on a container. The same-compilation expansion no longer double-reports: the container-side re-expansion of a same-compilation module''s [Scan] discards its diagnostics (the module pipeline already reports the identical set at the same location), and a same-compilation module''s [GeneratedScanRegistration] attributes are skipped during collection, which is a no-op for the generator (it never sees its own output) but keeps an analyzer running over the post-generation compilation from doubling every match in its graph.
|


A library can now keep implementations
internaland expose only interfaces: a[Module]that declares a[Scan]compiles that scan in its own build, emitting apublic staticfactory per match (which constructs the implementation and returns its accessible exposure interface) plus a single-arg[Singleton<IExposure>(Factory = …, Fallback = Silent)]registration into the module's partial. To a consuming container this is an ordinary module factory registration, so nothing new crosses the assembly boundary.[Module], gated on the module declaring a[Scan]; a plain module still emits nothing.internalmatches are seen), reusing the container scan's candidate discovery, filters and diagnostics.IDisposablebehind an interface is handled by the existing runtime disposal check for interface-returning factories.[Scan]is no longer rejected on import).Current limitations (each reported at the library's source): a match exposes through exactly one accessible interface, and its constructor parameters must be types a consumer can name. Version-skew detection is deferred. Self-compilation is a cross-assembly feature.
[Module]compile its own[Scan], so a library can keep implementations internal #113