## Summary `TemplateClassMemberParameter.Type` holds an `ITypeSymbol`. When an **aspect is declared in the project's own source** and one of its templates has a parameter whose type is **also declared in that source**, the symbol comes from the source compilation. The template members hang off `AspectClass`, which hangs off `AspectPipelineConfiguration.BoundAspectClasses`, and the configuration is deliberately long-lived at design time: it is reused for every new compilation and discarded only when compile-time code changes. The compilation from which the configuration was built is therefore pinned for as long as the solution is open, which is the retention that `design-time-memory.md` forbids. An aspect that comes from a referenced package is **not** affected, because its template parameter types are metadata symbols. ## Reproduction `UserCodeRetentionAnalyzerTests.AspectDeclaredInSourceWithATemplateParameter_IsRetainedThroughTheAspectClass` compiles: ```csharp internal class IntroducingAspect : TypeAspect { [Introduce] public void IntroducedMethod( TargetOne parameter ) { } // TargetOne is declared in the same project } ``` and asserts that the analysis finds exactly one retention attributed to Metalama. The chain: ``` fabric contributor #0 (AspectQuerySource<IDeclaration>) _aspectClass : AspectClass <Members>k__BackingField : ImmutableDictionary<String,TemplateClassMember> _value._firstValue.value : TemplateClassMember <Parameters>k__BackingField.array : TemplateClassMemberParameter[] [0] : TemplateClassMemberParameter <Type>k__BackingField : NonErrorNamedTypeSymbol ``` The chain arrives through a fabric contributor because that is one of the roots the diagnostic walks, but the `AspectClass` objects are reached just as directly from `AspectPipelineConfiguration.BoundAspectClasses`, so the retention does not depend on the project having a fabric. The test asserts the count is 1 rather than 0, in the manner of `MemoryLeakAssert.RetainedThrough`: when this issue is fixed the test fails, which is the signal to change the expected count, not to relax the assertion. ## Why the symbol comes from the source compilation `TemplateClassFactory` chooses the context in which the templates are reflected: ```csharp // Metalama.Framework.Engine/Aspects/TemplateClassFactory.cs:63 var templateDiscoveryContext = item.Project.TemplateReflectionContext ?? this.CompilationContext; ``` `CompileTimeProject.TemplateReflectionContext` comes from `CacheableTemplateDiscoveryContextProvider`, whose purpose is to be safe to cache. Its own summary says so: > This class provides a `ITemplateReflectionContext` that can be cached because the `Compilation` stores only references > to `PortableExecutableReference`, and nothing that holds a `SyntaxTree`. But `CreateContext` returns `null` unless the project references a package that carries aspects: ```csharp // Metalama.Framework.Engine/CompileTime/CacheableTemplateDiscoveryContextProvider.cs:46-51 else { // If we don't have external aspect PE references, we don't need a cacheable ITemplateReflectionContext. // We can always use the source context. return null; } ``` The comment is correct for a batch compilation, which handles one compilation and exits. It was not written with the design-time configuration cache in mind, and that is where it stops holding. `TemplateClassMemberParameter` is a `record` whose `Type` is `ITypeSymbol?` (`Metalama.Framework.Engine/CompileTime/TemplateClassMemberParameter.cs:12`), assigned from `parameterSymbol.Type` in `TemplateClassMemberBuilder`. ## What is established, and what is not - **Measured**: the reachability, by the retention diagnostic, on an aspect declared in source with a source-typed template parameter. Exactly one finding, with the chain above. - **Measured**: that an aspect from a referenced package produces none, which is what distinguishes this from the original, overstated version of this issue. - **Not measured**: the growth itself. `FabricMemoryLeakTests.WithFabric_InitialCompilationIsCollected` passes today, but its project declares no aspect with a template parameter, so it does not cover this case. A test in `Metalama.Framework.Tests.UnitTests/DesignTime/Pipeline/MemoryLeaks/` that edits run-time code in a project with such an aspect, and asserts that the superseded compilations are collected, would settle it and should be the first step. ## Possible directions 1. Build a cacheable template reflection context unconditionally, rather than only when the project references a package that carries aspects. This makes the comment quoted above true in every case, at the cost of one synthetic compilation per project. It needs a check that template matching against a metadata projection of the project's own compile-time assembly behaves as it does against source, which the referenced-package case already exercises through the whole binding machinery. 2. Store what the template parameters need in a form that reaches nothing, as `FabricDriver` now does with `SymbolId`. The parameter type is used for template matching, nullability resolution and type rewriting, at a dozen call sites, so this is the larger change of the two. The first looks smaller and more likely to be right, since the cacheable context already exists and is already trusted for the cross-assembly case. ## Related - #1802, #1805 (the diagnostic that found this, and the correction to its symbol rule) - #1793, #1799, #1796, #1797 (the design-time memory work) - `Metalama.Framework/docs/design-time-memory.md` — Claude for @gfraiteur