AOT generator: self-contained reflection-free attribute registration#9082
Conversation
…t.AotReflection.SourceGeneration The generated MSTestReflectionMetadata registry emitted invalid C# for some otherwise-valid [TestClass] shapes: - static properties produced ((T)instance).Prop (CS0176) - indexer properties produced ((T)instance).this[] (garbage) - set-only / non-readable properties emitted an unconditional getter Now static members are accessed through the type name, indexers are skipped (they cannot be modeled by the name-based Get/Set delegate shape), and the Get delegate throws when there is no accessible getter. Adds IsStatic and HasGettableValue to TestPropertyModel and three covering tests. Part of #1837. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ring MSTest.AotReflection.SourceGeneration emitted a reflection-free registry but no [ModuleInitializer], so referencing the package did nothing at runtime. Share MSTest.SourceGeneration's proven runtime-wiring source (ReflectionMetadataGenerator + ReflectionMetadataEmitter + TestAssemblyMetadata) into the AOT generator via compile-links so that a project referencing ONLY the AOT package is discoverable and runnable today (via ReflectionMetadataHook.Register + DynamicDependency rooting), on top of the reflection-free registry the future 0%-reflection path will consume. Shared source (not a fork) keeps the wiring from drifting until the reflection-free execution path is wired and the two generators are consolidated. Adds a unit test that verifies the emitted module initializer registers the assembly and compiles against the adapter hook (66/66 generator tests pass). Part of #1837. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replaces the shared MethodInfo-only wiring (from the previous commit) with a self-contained [ModuleInitializer] emitted by the AOT generator itself. On top of the type/test-method rooting + DynamicDependency that the shipping generator does, the registration now publishes pre-materialized type-level and assembly-level attributes, so the adapter serves them from source-generated data instead of calling GetCustomAttributes at runtime. - New ReflectionMetadataHook.Register overload (assembly, types, testMethods, typeAttributes, assemblyAttributes) populating the provider's TypeAttributes / AssemblyAttributes slots (existing 3-arg overload now delegates to it). Added to PublicAPI.Unshipped.txt. - New RuntimeRegistrationEmitter emits a single module initializer (no double registration): types, [TestMethod]s (ResolveMethod), DynamicDependency for class + accessible non-generic base types, and inline materialized type/assembly attributes. - AOT model gains IsTestMethod (base-chain aware, so [DataTestMethod] counts) and BaseTypeFullyQualifiedNames; removed the shared generator compile-links so the AOT generator is self-contained. Method attributes, constructors and properties still fall back to reflection (keyed by MethodInfo/PropertyInfo handles); closing those needs the execution-engine rework. 66/66 generator tests pass; adapter builds warning-clean (net8.0). Part of #1837. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🧪 Test quality grade — PR #90822 test method(s) graded across 1 file. Both receive B (80–89): assertion coverage is strong in both cases; the only consistent knock is body length inflated by embedded source-literal fixture strings, which is expected in source-generator unit tests but still triggers the >30-line medium deduction under the standard rubric.
This advisory comment was generated automatically. Grades are heuristic
|
…cgen-reflection-free-attrs # Conflicts: # test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs
There was a problem hiding this comment.
Pull request overview
This PR advances the NativeAOT enablement work (#1837) by making MSTest.AotReflection.SourceGeneration self-contained and starting a reflection-free runtime-consumption path for type-level and assembly-level attributes. It does so by emitting a single [ModuleInitializer] registration that calls a richer ReflectionMetadataHook.Register(...) overload, allowing the adapter to serve pre-materialized attributes without falling back to GetCustomAttributes.
Changes:
- Extend the adapter hook (
ReflectionMetadataHook.Register) to accept and store source-generated type/assembly attributes, and update API surface tracking. - Update the AOT generator’s model to track “is test method” and base type FQNs, and add a new
RuntimeRegistrationEmitterto emit the module initializer + attribute materialization. - Update unit tests to validate the new registration file, including attribute materialization and test-method filtering.
Show a summary per file
| File | Description |
|---|---|
| test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs | Updates generator tests to expect an additional generated source and validate attribute-aware registration emission. |
| src/Analyzers/MSTest.AotReflection.SourceGeneration/Model/TestClassModel.cs | Extends the generator model with IsTestMethod and BaseTypeFullyQualifiedNames. |
| src/Analyzers/MSTest.AotReflection.SourceGeneration/Generators/TestClassModelBuilder.cs | Populates the new model fields (base-type capture + test-method detection). |
| src/Analyzers/MSTest.AotReflection.SourceGeneration/Generators/RuntimeRegistrationEmitter.cs | New emitter producing the [ModuleInitializer] registration plus materialized type/assembly attributes. |
| src/Analyzers/MSTest.AotReflection.SourceGeneration/Generators/MSTestReflectionMetadataGenerator.cs | Wires registration emission into the incremental generator pipeline. |
| src/Analyzers/MSTest.AotReflection.SourceGeneration/Generators/MetadataRegistryEmitter.cs | Exposes helpers needed by the new registration emitter. |
| src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/SourceGeneratedReflectionOperations.cs | Documentation update describing the new AOT attribute-publishing behavior. |
| src/Adapter/MSTestAdapter.PlatformServices/SourceGeneration/ReflectionMetadataHook.cs | Adds the new overload and stores TypeAttributes / AssemblyAttributes in the provider. |
| src/Adapter/MSTestAdapter.PlatformServices/PublicAPI/PublicAPI.Unshipped.txt | Records the new public overload. |
Copilot's findings
- Files reviewed: 9/9 changed files
- Comments generated: 3
- `RuntimeRegistrationEmitter`: emit a distinct type name (`MSTestAotSourceGeneratedReflectionMetadata`) inside a distinct `...SourceGeneration.Generated.Aot` namespace so a compilation that references both this AOT generator and the shipping `MSTest.SourceGeneration` package doesn't end up with two generated types named `MSTestSourceGeneratedReflectionMetadata` in the same namespace (which would produce CS0101 / duplicate `[ModuleInitializer]` and fail to compile). - `ReflectionMetadataHook`/`CompositeSourceGeneratedReflectionDataProvider`: `Register` for the same assembly is no longer last-wins for assembly-level attributes. `CompositeState` now tracks a per-assembly cumulative `AssemblyAttributesByAssembly` dictionary that is the union of every `AssemblyAttributes` array published for a given assembly, and `GetAssemblyAttributes` returns from that union. Re-registering the same assembly from multiple module initializers (or from a future generator composition path) no longer silently drops attributes published by an earlier registration. - `MSTestReflectionMetadataGenerator_AotMode_EmitsModuleInitializerAndPublishesAttributes` test: also assert `result.Diagnostics.Should().BeEmpty()` so future generator changes that emit warnings/errors don't pass silently as long as the emitted code happens to compile. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stacked on #9078.
Makes the AOT generator self-contained and starts the reflection-free runtime-consumption path: instead of borrowing
MSTest.SourceGeneration's MethodInfo-only wiring (#9078), the generator now emits its own single[ModuleInitializer]that, on top of the type/test-method rooting +[DynamicDependency], publishes pre-materialized type-level and assembly-level attributes. The adapter serves those from source-generated data instead of callingGetCustomAttributesat runtime.Changes
ReflectionMetadataHook.Register(assembly, types, testMethods, typeAttributes, assemblyAttributes)overload populating the provider'sTypeAttributes/AssemblyAttributesslots (the existing 3-arg overload now delegates to it). Added toPublicAPI.Unshipped.txt.SourceGeneratedReflectionOperationsalready reads those slots, so type/assembly attribute reads stop falling back to reflection.RuntimeRegistrationEmitteremits exactly one module initializer (no double-registration):types,[TestMethod]s viaResolveMethod,[DynamicDependency]for each class + its accessible non-generic base types, and inline-materialized type/assembly attributes. Removed the shared-source compile-links from Make AOT generator functional: emit the proven [ModuleInitializer] wiring #9078 — the AOT generator is now self-contained.TestMethodModel.IsTestMethod(base-chain aware, so[DataTestMethod]and other subclasses count) andTestClassModel.BaseTypeFullyQualifiedNames.Still falls back to reflection (next steps)
Method attributes, constructors, and properties are keyed by
MethodInfo/PropertyInfo/ConstructorInfohandles, which can't be produced reflection-free; eliminating those is the execution-engine rework (TestMethodInfoinvokes viaMethodInfo.Invoke; instantiation viaActivator). Tracked for follow-up.Tests
Generator_EmitsModuleInitializer_RegisteringAssemblyWithAttributes_AndCompilesAgainstHookverifies the single initializer compiles against the adapter hook, materializes[TestCategory]reflection-free, and registers only[TestMethod]s. 66/66 generator tests pass; adapter builds warning-clean (net8.0).Part of #1837.