feat: allow lifecycle hooks to take graph-resolved parameters#110
Conversation
OnActivated/OnRelease hooks may now declare parameters after the leading instance parameter; each is resolved from the container graph exactly like a constructor or factory parameter. Activation dependencies are passed inline at the call; release dependencies are resolved at construction and captured by value into the queued closure, so reverse creation-order teardown keeps them alive until the release has run. Hook parameters are full graph dependencies: AWT101 (missing), cycle, captive-dependency and async-taint analysis all account for them. A runtime [Arg] hook parameter is rejected with the new AWT189.
🚀 Benchmark ResultsDetails
Details
Details
|
… pass The graph-resolved parameters of OnActivated/OnRelease hooks were classified as full graph dependencies but only registered as edges in BuildEdges, so every other pass that walks an instance's constructor parameters skipped them. That let a hook parameter escape validation and the emitted infrastructure it needs, producing generated code that does not compile with no Awaiten diagnostic to explain it: an [ImportService<T>] hook parameter emitted a __ResolveExternal call the container never declared (CS1061); a plain hook parameter over a parameterized target missed AWT115 (CS7036); an Owned<T> hook parameter over a requesting-type factory missed AWT186 (CS1503); a synchronous Func<T> hook parameter over an async-tainted target missed AWT119/120 (CS0103); and a collection hook parameter with an async-initialized member missed AWT122 (CS0117). An [ImportService<T>] consumed only by a hook parameter was also wrongly reported unconsumed (AWT176). Add InstanceModel.HookParameters() (activation plus release parameters) and visit it wherever ConstructorParameters is walked: external-dependency advertisement and the __ResolveExternal/__AsyncArray emit gates, AWT115/118/119/120/122/186 analysis, the unconsumed-import check, the fresh-disposable transient walk, and the captive-dependency message naming. BuildEdges now uses the same helper instead of iterating the two arrays separately. Cover each failing scenario with a hook-parameter test in its owning diagnostic file, and add runtime tests proving an external hook parameter both compiles and resolves through the external resolver and is advertised as an external dependency.
…ease-order test A lifecycle hook is reached by simple name, so when a registration names an overloaded method whose overloads all accept the instance as their first parameter, the container's choice - and the graph dependencies the extra parameters resolve - was silently order-dependent. Collect every accessible static void method of that name accepting the instance and, when more than one qualifies, report AWT190 and emit no hook, mirroring how AWT112 handles an ambiguous factory. One match is used as before; zero remains AWT164. Also make ReleaseHookDependency_OutlivesTheRelease_InReverseCreationOrder actually exercise the release capture. PooledBuffer previously took the Pool in its constructor, which forced Pool-first creation on its own and would pass even if the capture ordering were wrong. Drop that constructor dependency so the reverse-teardown order is produced solely by the release capture resolving (and first-constructing, and queuing the release of) the Pool before the buffer's own release is enqueued - verified against the generated code.
ClassifyHookParameters mirrored the constructor/factory classification pipeline but omitted its three key-misuse reporters, so a mistake on a hook parameter that is rejected on a constructor parameter went undiagnosed: a [FromKey] whose constant is of an unsupported key type (AWT170), a synthesized keyed collection requested under an unsupported key type (AWT159), and a [FromKey] on a synthesized keyed collection (AWT160). Run all three from ClassifyHookParameters at the pipeline positions the constructor path uses - AWT170 straight after classification, AWT159/AWT160 after SuppressRegisteredCollectionSynthesis so an explicitly registered dictionary rewritten to Direct is not reported. AWT170 is reported after the [Arg] rejection rather than before it, so a hook parameter already rejected with AWT189 does not collect a second diagnostic. Cover each with a hook-parameter test. Also stop HookParameters() allocating on every call. It is invoked from every dependency-walking pass, several of them per-instance loops on the analyzer's per-keystroke path, and EquatableArray.AsArray() is already zero-alloc for an empty array. Return a backing array directly when either hook contributes no parameters (the overwhelmingly common case) and build a new array only when both do. A cached field is deliberately not used: record equality compares all instance fields, so a lazily populated cache would make two equal models compare unequal and break incremental-generation caching. Document that [RequestingType] is factory-only, so like a constructor parameter it is not honored on a hook parameter: a hook is invoked by the container for an instance rather than requested by a consumer, so there is no requesting consumer whose typeof(...) it could receive.
A release hook's dependencies are captured at construction, but a Func<T>/Lazy<T> parameter captures only a resolver delegate, and the hook runs during its owner's teardown, after __disposed is set - so invoking the delegate there throws ObjectDisposedException every time, making the parameter unusable at the only moment the hook runs. Report AWT191 (error) for the four deferred-delegate kinds (Func, Lazy, FuncTask, LazyTask, covering their Task and Owned forms) on OnRelease hook parameters. An OnActivated hook runs while the owner is alive, so its Func/Lazy parameters remain allowed. IAsyncEnumerable<T> needs no rejection: it materializes its members eagerly at capture time. Unlike a rejected [Arg] (AWT189), the parameter is kept rather than dropped: it resolves like any graph dependency, so the emitted capture and every downstream dependency-walking pass stay coherent while the error fails the build. The AWT118 analyzer test's fixture was a release hook holding a Func over a disposable transient - exactly the shape AWT191 now rejects, and the analyzer harness requires its source to compile. Move the scenario to an activation hook, which is the surviving hook-parameter shape that walk covers (a root-owned singleton's activation hook can invoke its Func, each call tracking a fresh disposable on the root), and update the analyzer comment that cited the release-capture rationale. Also close the runtime coverage gaps on the hook-parameter emit paths: an async-initialized activation hook dependency is awaited (initialized) before the hook runs on the async caching path, an async release capture is awaited at construction on the async fresh path, and a scoped instance's release capture goes through the cached release-registration path and runs when its scope is disposed.
…endencies() Routing lifecycle hook parameters through every dependency-walking pass added a third sibling foreach (constructor parameters, injected [Inject] members, hook parameters) to each of them, pushing DetectSynchronousAsyncResolution, DetectSynchronousAsyncCollection and ExternalDependencies over Sonar's cognitive-complexity threshold of 15 (16, 17 and 19). Add InstanceModel.WalkedDependencies(), enumerating the three groups in the order the passes already visited them (constructor parameters, member dependencies, hook parameters), and collapse each pass to a single loop over it - so diagnostic order and ExternalDependencies' first-seen dedup order are unchanged. DetectOwnedOverRequestingTypeFactory was below the threshold but shares the identical shape, so it is converted too rather than left as the one hand-rolled copy of the pattern. Passes that treat the groups differently (deferred members in BuildEdges, the [Arg]-index-sensitive walks) keep enumerating them separately, as the helper's doc note spells out.
|
…ph-resolved parameters (#110) by Valentin Breuß
…ph-resolved parameters (#110) by Valentin Breuß
|
This is addressed in release v0.6.0. |



OnActivated/OnRelease hooks may now declare parameters after the leading instance parameter; each is resolved from the container graph exactly like a constructor or factory parameter. Activation dependencies are passed inline at the call; release dependencies are resolved at construction and captured by value into the queued closure, so reverse creation-order teardown keeps them alive until the release has run.
Hook parameters are full graph dependencies: AWT101 (missing), cycle, captive-dependency and async-taint analysis all account for them. A runtime [Arg] hook parameter is rejected with the new AWT189.