AOT generator: self-contained reflection-free attribute registration#9082
AOT generator: self-contained reflection-free attribute registration#9082Evangelink wants to merge 5 commits into
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
| internal static class RuntimeRegistrationEmitter | ||
| { | ||
| private const string GeneratedTypeName = "MSTestSourceGeneratedReflectionMetadata"; | ||
|
|
||
| public static string Emit(AssemblyMetadataModel assemblyMetadata, IReadOnlyList<TestClassModel> testClasses) |
There was a problem hiding this comment.
Good catch -- fixed in 20251d8. The AOT generator now emits its type into a distinct ...SourceGeneration.Generated.Aot namespace and renames the type to MSTestAotSourceGeneratedReflectionMetadata, so referencing both this generator and the shipping MSTest.SourceGeneration package in the same compilation no longer yields duplicate type definitions or competing module initializers. Review reply handled.
| GeneratorRunResult result = driver.GetRunResult().Results[0]; | ||
| result.Diagnostics.Should().BeEmpty(); | ||
|
|
There was a problem hiding this comment.
Added in 20251d8 -- the test now asserts result.Diagnostics.Should().BeEmpty() after the generator run, guarding against future generator changes emitting warnings/errors that would currently slip through as long as the emitted code still compiles. Review reply handled.
| /// <b>Infrastructure.</b> Publishes source-generated metadata for <paramref name="assembly"/> | ||
| /// to the MSTest adapter, including pre-materialized type-level and assembly-level attributes | ||
| /// so the adapter serves them without runtime reflection. Safe to call from multiple module | ||
| /// initializers; later registrations are merged with earlier ones. | ||
| /// </summary> |
There was a problem hiding this comment.
Implemented true per-assembly merging in 20251d8. CompositeState now tracks an AssemblyAttributesByAssembly dictionary that is the running union of every AssemblyAttributes array registered for a given assembly; GetAssemblyAttributes(assembly) returns from that union. Re-registering the same assembly (multiple module initializers, manual calls, or future generator composition) now preserves the attributes from earlier registrations instead of silently dropping them. Review reply handled.
- `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.