feat: support lifecycle hooks on a [Module]'s self-compiled [Scan]#127
Conversation
A [Module] could self-compile its own [Scan], and a [Scan] could name OnActivated/OnRelease hooks, but the two did not meet: a hook named on a module scan was silently dropped. This wires them together while keeping the module's hook method and implementation internal. Each module scan match now emits, next to its factory, a public static wrapper that casts the exposure interface to the hook's first-parameter type and calls the user hook. A generic hook is closed at module build from the match's marker form, so the wrapper stays non-generic and a consumer in another assembly never sees generics or internal types on the signature. The wrapper name is carried on the emitted [GeneratedScanRegistration] through new OnActivated/OnRelease properties; the consumer reads them and the existing hook pipeline binds the wrapper and resolves its post-instance dependencies from the consumer's graph. Hooks are resolved and validated in the module's own build, reusing the container hook diagnostics (AWT164/189/190/191/198) so errors land at the library source. Because a hook's post-instance dependencies appear on the public wrapper signature and are resolved by the consumer, each must be externally accessible; a dependency that is not is reported as AWT203. The hook method itself and the instance parameter may stay internal, since the wrapper reaches them from inside the module. The same-compilation direct-expand path sets Origin to the module so ResolveHook binds the module's own internal hook directly. Adds generator tests for wrapper emission and the module-build diagnostics, and cross-assembly plus same-compilation runtime tests exercising internal non-generic and generic hooks firing on activation and release.
🚀 Benchmark ResultsDetails
Details
Details
|
Review fixes for lifecycle hooks on a [Module]'s self-compiled [Scan]: - Hook slots on ImplInfo now carry their own origin: a hook name is owner-relative, so a same-compilation module scan's hook filling a slot on an ImplInfo whose Origin an earlier (container) scan fixed no longer resolves against the wrong owner (spurious AWT164, or silently binding a same-named container method). The same name on a different origin is a different method and conflicts (AWT199, loser module-qualified in the message) instead of merging, which also stops a losing scan from polluting another origin's closed-marker set (spurious AWT198). - A second module [Scan] overlapping the same match now merges its hooks per slot like two container scans (fill empty slot, restate unions marker forms and re-resolves, conflict is AWT199) instead of silently dropping them at the overlap dedup. - AWT204 (new): a module hook dependency parameter marked [FromKey] or [Inject] is rejected - the wrapper's bare signature would silently drop the attribute cross-assembly while a same-compilation container honored it (the hook twin of the factory's AWT200). - A hook with a by-ref (ref/in/out) parameter is no longer a usable module hook (AWT164) instead of generating uncompilable wrapper code. - The wrapper's synthetic instance parameter is suffixed away from a mirrored user parameter literally named awaiten__instance (CS0100). - RawRegistration/ModuleScanExpansion carry closed marker forms per hook slot, so differently-markered hooks merged from two scans re-resolve exactly as the module build decided. - Sonar: ResolveModuleHook bundles (module, compilation, diagnostics) into a ModuleScanContext (8 -> 5 parameters); TryMirrorHookParameters drops its unused Compilation parameter.
The overlap slot merge re-resolves a hook a second scan names for an unclaimed slot, so a hook that already failed at the first scan (AWT164/ AWT189/AWT191/AWT198/AWT203/AWT204) was diagnosed again at every overlapping scan restating the same name. The module-scan context now records failed attempts per (match, slot, name); a repeated attempt still resolves (the merge needs the outcome) but reports into a discarded sink, while a fresh failure - a new name, or a restated hook newly ambiguous over widened markers - reports normally. Found by a follow-up review of the previous commit.
…cans A hook that failed to resolve used to leave (or wipe) its slot unclaimed, discarding the accumulated closed-marker union: a later overlapping [Scan] restating the same name then re-resolved over only its own markers and could emit a fully wired wrapper bound to a scan-order-dependent generic closing beside the already-reported AWT198, and a later scan naming a different method bypassed the AWT199 conflict check entirely. A failed slot now keeps its name and marker union with only the wrapper absent, so a restatement re-resolves over the true union, a different name still conflicts, and same-compilation wiring (gated on the wrapper) never binds a failed hook. Covered by regression tests for the two-scan reopen, the three-scan wipe-then-reopen, and the conflict-after-failure cases. Review cleanups alongside: the AWT190 message no longer blames a container that cannot choose (it is also reported at module build time, where no container exists), with its XML doc, docs entry and release note aligned; the AWT199 XML doc and docs entry now state that hook names are owner-relative, matching the cross-origin conflict the coalescer already reports; the emitted wrapper XML summary says it casts to the hook instance parameter type, which is what the code does for generic and base-typed hooks alike; the AWT203/AWT204 docs snippets declare Roaster so they are self-contained, and the AWT204 entry drops its em-dash. Test and lint housekeeping: the AwtNNN diagnostic tests move out of SelfCompiledModuleScanHookTests into the DiagnosticTests.AwtNNN partial-class files (new files for AWT203/AWT204), TryMirrorHookParameters delegates per-parameter validation to a helper to cut its cognitive complexity, and EmitModuleHook builds the synthetic instance name with a StringBuilder.
The wrapper-gating ternaries added for failed hook slots pushed Collect past the cognitive complexity limit, so the module-scan expansion loop moves into CollectSameCompilationModuleScans with its explanatory comment as the XML doc. No behavior change.
…res it ResolveModuleHook recorded a failed (match, slot, name) attempt to suppress repeat diagnostics across overlapping scans, but never cleared it when a restatement over a widened marker union succeeded. A still-later scan turning that union ambiguous then failed into the discarded sink: the hook was silently unwired while the only reported diagnostic stayed the first scan's AWT164, which no longer described the slot. A successful re-resolution now removes the record, so the record tracks the latest outcome and a fresh failure reports normally.
…a helper The four hook ternaries sat two loops deep in CollectSameCompilationModuleScans, each paying the nesting penalty and pushing the method's cognitive complexity to 17 (Sonar allows 15). The RawRegistration construction moves to an expression-bodied SameCompilationScanRegistration helper carrying the hook-wiring rationale as its summary, leaving the loop at complexity 5 and the helper at 4. No behavior change.
|
…dule]`'s self-compiled `[Scan]` (#127) by Valentin Breuß
…dule]`'s self-compiled `[Scan]` (#127) by Valentin Breuß
|
This is addressed in release v0.7.0. |



A
[Module]could self-compile its own[Scan], and a[Scan]could nameOnActivated/OnReleasehooks, but the two did not meet: a hook named on a module scan was silently dropped. This wires them together while keeping the module's hook method and implementation internal.Each module scan match now emits, next to its factory, a public static wrapper that casts the exposure interface to the hook's first-parameter type and calls the user hook. A generic hook is closed at module build from the match's marker form, so the wrapper stays non-generic and a consumer in another assembly never sees generics or internal types on the signature. The wrapper name is carried on the emitted
[GeneratedScanRegistration]through newOnActivated/OnReleaseproperties; the consumer reads them and the existing hook pipeline binds the wrapper and resolves its post-instance dependencies from the consumer's graph.Hooks are resolved and validated in the module's own build, reusing the container hook diagnostics (AWT164/189/190/191/198) so errors land at the library source. Because a hook's post-instance dependencies appear on the public wrapper signature and are resolved by the consumer, each must be externally accessible; a dependency that is not is reported as AWT203. The hook method itself and the instance parameter may stay internal, since the wrapper reaches them from inside the module. The same-compilation direct-expand path sets Origin to the module so
ResolveHookbinds the module's own internal hook directly.Adds generator tests for wrapper emission and the module-build diagnostics, and cross-assembly plus same-compilation runtime tests exercising internal non-generic and generic hooks firing on activation and release.
[Scan]#112