Skip to content

AOT generator: self-contained reflection-free attribute registration#9082

Open
Evangelink wants to merge 5 commits into
mainfrom
dev/amauryleve/aot-srcgen-reflection-free-attrs
Open

AOT generator: self-contained reflection-free attribute registration#9082
Evangelink wants to merge 5 commits into
mainfrom
dev/amauryleve/aot-srcgen-reflection-free-attrs

Conversation

@Evangelink

Copy link
Copy Markdown
Member

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 calling GetCustomAttributes at runtime.

Changes

  • Adapter: new ReflectionMetadataHook.Register(assembly, types, testMethods, typeAttributes, assemblyAttributes) overload populating the provider's TypeAttributes / AssemblyAttributes slots (the existing 3-arg overload now delegates to it). Added to PublicAPI.Unshipped.txt. SourceGeneratedReflectionOperations already reads those slots, so type/assembly attribute reads stop falling back to reflection.
  • Generator: new RuntimeRegistrationEmitter emits exactly one module initializer (no double-registration): types, [TestMethod]s via ResolveMethod, [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.
  • Model: TestMethodModel.IsTestMethod (base-chain aware, so [DataTestMethod] and other subclasses count) and TestClassModel.BaseTypeFullyQualifiedNames.

Still falls back to reflection (next steps)

Method attributes, constructors, and properties are keyed by MethodInfo/PropertyInfo/ConstructorInfo handles, which can't be produced reflection-free; eliminating those is the execution-engine rework (TestMethodInfo invokes via MethodInfo.Invoke; instantiation via Activator). Tracked for follow-up.

Tests

Generator_EmitsModuleInitializer_RegisteringAssemblyWithAttributes_AndCompilesAgainstHook verifies 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.

Evangelink and others added 3 commits June 12, 2026 13:36
…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>
@Evangelink

Copy link
Copy Markdown
Member Author

🧪 Test quality grade — PR #9082

2 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.

ΔTestGradeBandNotes
mod MSTestReflectionMetadataGeneratorTests.
Generator_
EmitsModuleInitializer_
RegisteringAssemblyWithAttributes_
AndCompilesAgainstHook
B 80–89 Rich assertions covering attribute materialization, method-exclusion (negative), and compilation; ~50-line body with embedded fixture keeps it out of A.
mod MSTestReflectionMetadataGeneratorTests.
Generator_
IsIncremental_
SupportTypesAreCached_
WhenInputUnchanged
B 80–89 Multi-faceted assertions; count updated to 3 and well-documented in comment; body slightly over 30 lines due to embedded fixture string.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Grade Tests on PR (on open / sync) workflow. · 293.5 AIC · ⌖ 13.1 AIC · [◷]( · )

Base automatically changed from dev/amauryleve/aot-srcgen-functional-wiring to dev/amauryleve/aot-srcgen-property-lowering June 12, 2026 13:45
Base automatically changed from dev/amauryleve/aot-srcgen-property-lowering to main June 12, 2026 13:45
…cgen-reflection-free-attrs

# Conflicts:
#	test/UnitTests/MSTest.AotReflection.SourceGeneration.UnitTests/MSTestReflectionMetadataGeneratorTests.cs
Copilot AI review requested due to automatic review settings June 12, 2026 15:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RuntimeRegistrationEmitter to 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

Comment on lines +21 to +25
internal static class RuntimeRegistrationEmitter
{
private const string GeneratedTypeName = "MSTestSourceGeneratedReflectionMetadata";

public static string Emit(AssemblyMetadataModel assemblyMetadata, IReadOnlyList<TestClassModel> testClasses)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 2157 to 2158
GeneratorRunResult result = driver.GetRunResult().Results[0];
result.Diagnostics.Should().BeEmpty();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +68 to +72
/// <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>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants