Add WinRT.Generator.Core shared library and route all generators through it#2432
Draft
Sergio0694 wants to merge 27 commits into
Draft
Add WinRT.Generator.Core shared library and route all generators through it#2432Sergio0694 wants to merge 27 commits into
Sergio0694 wants to merge 27 commits into
Conversation
Three of the five generators (Projection, ProjectionRef, WinMD) already skipped blank lines in their response-file parsers, with an explanatory comment noting that the MSBuild 'ToolTask' infrastructure may emit them. The remaining two (Impl, Interop) were ported earlier, before this MSBuild quirk was characterized, and would instead throw 'MalformedResponseFile' on a blank line. The strict behavior was never deliberate — it's the original implementation that wasn't updated — and it's a latent crash waiting to surface the moment a task wrapper emits a blank line (e.g. for grouping or as a side effect of an empty optional value). Switches Impl + Interop to the same defensive ''skip blank lines'' branch the other three already use, with the same comment, so every parser is now byte-identical at that loop. WinMD already had the behavior but used 'string.IsNullOrEmpty(trimmedLine)' — converted to 'trimmedLine.Length == 0' to match the other four exactly. This also removes the only behavioral booby-trap that would have required a per-tool hook in the upcoming shared-code extraction work. Observable change: a blank line in a '.rsp' that previously caused 'WellKnownImplException(CSWINRTIMPLGEN0003)' or 'WellKnownInteropException(CSWINRTINTEROPGEN0003)' is now silently no-op. That error path is unreachable in normal operation (no task wrapper currently emits blank lines), so this is a defensive fix with no real-world regression risk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Creates a new internal shared library 'WinRT.Generator.Cli' to host code that was previously duplicated across the five Native AOT CLI generator projects (Impl, Interop, Projection, ProjectionRef, WinMD). This first phase does only the mechanical, behavior-preserving extractions:
- Path/File extensions (G1): 'PathExtensions' and 'FileExtensions' move out of 'WinRT.Interop.Generator/Extensions/' (where four other tools were reaching in via '<Compile Include=...\\Link=...>') and into 'WinRT.Generator.Cli/Extensions/'. The two ad-hoc cross-project links are dropped and replaced by a normal 'ProjectReference'. - Command-line argument name attribute (A1): five identical copies of 'CommandLineArgumentNameAttribute' collapse into one in 'WinRT.Generator.Cli/Attributes/'. - JSON serializer context (E1): five identical 'X*GeneratorJsonSerializerContext' copies (each carrying the same 'JsonSerializable(typeof(Dictionary<string,string>))') collapse into a single shared 'GeneratorJsonSerializerContext' in 'WinRT.Generator.Cli/Helpers/'.
Visibility is preserved ('internal') with 'InternalsVisibleTo' for each of the five consumer assemblies. Namespaces follow the existing per-project convention: extensions stay at the root namespace ('WindowsRuntime.GeneratorCli'), while attributes and helpers go into matching subnamespaces. The new library is wired into 'src/cswinrt.slnx'.
Two ancillary build fixes were needed to unblock validation:
- 'WinRT.Impl.Generator' and 'WinRT.Interop.Generator' csproj files gain a scoped '<NoWarn>IDE0028(;IDE0370)</NoWarn>' for the same SDK analyzer regression already handled in 'WinRT.WinMD.Generator' and 'WinRT.Projection.Generator' (the 'new(StringComparer.Ordinal)' pattern can't be simplified to a collection literal without dropping the comparer). - Five Interop files that previously relied on implicit namespace resolution to pick up 'Path.Normalize'/'Path.IsWithinDirectoryName'/'File.ReadAllLines(Stream)' from the sibling namespace ('IgnoresAccessChecksToBuilder', 'InteropGenerator', 'InteropGenerator.Emit', 'InteropGenerator.DebugRepro', 'InteropGeneratorArgs.Parsing') gain an explicit 'using WindowsRuntime.GeneratorCli;'.
Validated end-to-end with debug-repro round-trips: ProjectionRef 8/8 .cs files 0 diffs, WinMD 5632/5632 bytes 16-byte (MVID-only) diff, Projection 327/327 .cs files 0 diffs, Impl 257536/257536 bytes 0 diffs, Interop smoke-tested. All 5 generators build with 0 warnings in Release and AOT-publish cleanly for win-arm64.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the shared error contract that all five CsWinRT CLI generators inherit from, into 'WinRT.Generator.Cli/Errors/':
- 'WellKnownGeneratorException' (D1): abstract base capturing the 'Id' field plus the simple-form 'ToString()' that all four non-Interop tools were duplicating verbatim. - 'UnhandledGeneratorException' (D2): abstract base capturing the phase field plus the standardized error-message 'ToString()'. Per-tool subclasses provide 'ErrorPrefix' and 'GeneratorDescription' (e.g. 'impl generator', 'WinMD generator'). 'QuotePhaseInMessage' is virtual and defaults to false; the interop generator overrides it to true to preserve its historical single-quoted phase formatting. - 'GeneratorExceptionExtensions.IsWellKnown' (D3): the shared cancellation/well-known-base predicate. Per-tool 'IsWellKnown' extensions are deleted.
Each per-tool 'WellKnown<X>Exception' shrinks to a primary-constructor sealed subclass. Interop's 'WellKnownInteropException' keeps its rich superset locally ('_outerException' field, custom 'ToString' that adds inner/outer context, 'ThrowOrAttach' with '[StackTraceHidden]' + '[DoesNotReturn]') — it just delegates the 'Id' + ctor wiring to the shared base.
Each per-tool 'Unhandled<X>Exception' shrinks to a primary-constructor sealed subclass overriding the 2-3 abstract/virtual members. The standardized 'ToString()' output is bit-identical to the previous per-tool implementations (verified).
Each consumer that uses '.IsWellKnown' gains a 'using WindowsRuntime.GeneratorCli.Errors;' import; the per-tool '*ExceptionExtensions.cs' files (5) are deleted.
Validated end-to-end with debug-repro round-trips for all 5 generators: ref-gen 8 files 0 diffs, WinMD 5632 bytes (16-byte MVID-only delta), projection 327 files 0 diffs, impl 257536 bytes 0 diffs, interop smoke-tested. All 5 build with 0 warnings in Release and AOT-publish cleanly for win-arm64.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 'IGeneratorErrorFactory' (a 'static abstract' interface in 'WinRT.Generator.Cli/Errors/') with the six logical errors that every CLI generator throws today through its 'WellKnown*Exceptions' factory: - 'ResponseFileReadError(Exception)' - 'ResponseFileArgumentParsingError(string, Exception?)' - 'MalformedResponseFile()' - 'DebugReproDirectoryDoesNotExist(string)' - 'DebugReproMissingFileEntryMapping(string)' - 'DebugReproUnrecognizedFileEntry(string)' All return 'Exception', so the per-tool numeric ID, message text, and concrete 'WellKnown*Exception' subtype remain the sole responsibility of each tool's factory. This is the contract that Phase 4 (response-file plumbing) and Phase 5 (debug-repro helpers) will dispatch through, so shared code can throw via the right per-tool factory without ever knowing the IDs or message strings. Each per-tool 'WellKnown*Exceptions' shifts from 'internal static class' to 'internal sealed class' implementing 'IGeneratorErrorFactory', with a private constructor to prevent instantiation. Every factory body, error ID, message text, and concrete exception type is preserved verbatim — including the WinMD swap of 'MalformedResponseFile' (id 2) and 'ResponseFileArgumentParsingError' (id 3) versus the other four tools, and the embedded per-tool exe names in 'ResponseFileReadError'. Interop is special: its public static factories return 'WellKnownInteropException' (typed, so callers chain '.ThrowOrAttach(...)'). C# 'static abstract' interface methods don't support return-type covariance, so Interop uses explicit interface implementations that forward through the typed publics — preserving both call shapes. Validated end-to-end: ref-gen 8 files 0 diffs, WinMD 5632 bytes (16-byte MVID-only), projection 327 files 0 diffs, impl 257536 bytes 0 diffs, interop smoke-tested. All 5 build clean (0 warnings) in Release and AOT-publish cleanly for win-arm64. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 'ResponseFileParser<TArgs, TErr>' and 'ResponseFileBuilder<TArgs>' in 'WinRT.Generator.Cli/Parsing/', collapsing four candidates (B1 parsing core, B2 typed Get* helpers, B3 reflection lookup, C1 formatting) into one reflection-based mechanism that walks 'Type.GetProperties()' once per call.
Per-property handling:
- '[CommandLineArgumentName("--xxx")]' → CLI flag name. - 'required' modifier (via 'RequiredMemberAttribute') → throw 'TErr.ResponseFileArgumentParsingError(prop.Name)' if missing or parse fails. - 'string?' (via 'NullabilityInfoContext') → null on miss. - '[DefaultValue("…")]' (only on 'ProjectionGeneratorArgs.AssemblyName') → applied when missing. - 'CancellationToken'-typed property → set from parser's token argument. - 'Convert.ChangeType(string, propertyType, InvariantCulture)' for primitives ('bool', 'int'). - 'string[]' → 'Split(',', RemoveEmptyEntries | TrimEntries)'.
The parser uses 'RuntimeHelpers.GetUninitializedObject' to bypass the runtime 'RequiredMemberAttribute' enforcement at construction time, then populates each public property via 'PropertyInfo.SetValue'. All AOT-safe under '[DynamicallyAccessedMembers(PublicProperties)]' on 'TArgs'.
Each per-tool 'XGeneratorArgs.cs' loses its 'partial' modifier and gains three thin forwarding wrappers ('ParseFromResponseFile(string|Stream, CancellationToken)' and 'FormatToResponseFile()') so call sites are unchanged. The corresponding '*Args.Parsing.cs' and '*Args.Formatting.cs' partial files (10 files total across the 5 generators) are deleted entirely.
All exceptions continue to route through the per-tool 'WellKnown*Exceptions' factory via 'TErr : IGeneratorErrorFactory', so error IDs (including WinMD's '2'/'3' swap), messages (including embedded 'cswinrtimplgen' etc. tool names), and concrete exception types are preserved bit-identically. Optional 'bool' / 'int' / 'string[]' parse failures silently fall back to defaults (matching the old 'GetOptional*' behavior); required parse failures throw (matching the old 'GetBooleanArgument' / 'GetInt32Argument' / 'GetStringArrayArgument' behavior).
Validated end-to-end with debug-repro round-trips for all 5 generators: ref-gen 8/8 files 0 diffs, WinMD 5632/5632 bytes 16-byte (MVID-only) delta, projection 327/327 files 0 diffs, impl 257536/257536 bytes 0 byte diffs, interop smoke-tested. All 5 build with 0 warnings in Release and AOT-publish cleanly for win-arm64.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 'DebugReproPacker' (in 'WinRT.Generator.Cli/DebugRepro/') with the five small leaf helpers that were duplicated near-byte-identically across every '*Generator.DebugRepro.cs':
- 'GetHashedFileName(string)' - Shake128 hash → '{name}_{HEX}{ext}' - 'CopyHashedFilesToDirectory(string[], string, Dictionary, CancellationToken)' - 'CopyHashedFileToDirectory(string?, string, Dictionary, CancellationToken)' (simple variant) - 'CopyPathMapToDirectory(Dictionary, string, string)' - JSON serialize via shared 'GeneratorJsonSerializerContext' - 'ExtractPathMap(ZipArchiveEntry)' - JSON deserialize via the same context
Each per-tool '*Generator.DebugRepro.cs' loses its five private helper definitions and gains 'using WindowsRuntime.GeneratorCli.DebugRepro;' plus the 'DebugReproPacker.' qualifier on call sites. Unused 'using' statements (for 'System.Security.Cryptography', 'System.Text', 'System.Text.Json', 'GeneratorJsonSerializerContext', etc.) are removed.
Interop keeps its rich 'CopyHashedFileToDirectory' local. That variant has reserved-DLL dedupe logic plus 'WellKnownInteropExceptions.ReservedDllOriginalPathMismatchFromDebugRepro(fileName)' on path mismatch — neither generalizable nor shared. It now delegates the hash computation to 'DebugReproPacker.GetHashedFileName'.
Hashed filenames are content-addressed via Shake128 of the original file path, so cross-version replay continues to work: a '.zip' produced by an older build can be unpacked by the new build and vice versa.
Validated end-to-end: ref-gen 8/8 files 0 diffs, WinMD 5632/5632 bytes 16-byte MVID-only delta, projection 327/327 files 0 diffs, impl 257536/257536 bytes 0 byte diffs, interop smoke-tested. All 5 build with 0 warnings in Release and AOT-publish cleanly for win-arm64.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…repare
Adds 'IGeneratorArgs' (a marker interface with the two well-known properties: 'Token' and 'DebugReproDirectory') and 'GeneratorHost' (in 'WinRT.Generator.Cli/'), both encapsulating the unpack-debug-repro -> parse-response-file -> save-debug-repro preamble that every generator's 'Run' method previously had inline.
Each generator's 'Run' method shrinks from ~80 LOC to ~12 LOC: a single call to 'GeneratorHost.Prepare<TArgs>(...)' with per-tool callbacks (unpack/parse/save delegates + 'wrapUnhandled' lambda that constructs the per-tool 'Unhandled*Exception' + 'ConsoleApp.Log' for the progress messages + the tool name for the log strings).
All five args records gain ': IGeneratorArgs' — no new properties are required because each already has 'public required CancellationToken Token { get; init; }' and 'public string? DebugReproDirectory { get; init; }' with matching signatures.
Behavior preserved exactly:
- Same phase names ('unpack-debug-repro', 'parsing', 'save-debug-repro') routed through each tool's per-tool 'Unhandled*Exception' wrapper. - Same log messages, byte-identical (the shared formatter substitutes the tool name via interpolation). - Same cancellation boundaries at each phase. - Same 'isUsingDebugRepro' flag controlling save-on-replay suppression. - Same 'DebugReproDirectory is not null' guard. - 'static (phase, e) => new Unhandled*Exception(phase, e)' lambdas — the 'static' modifier ensures no closure allocations per call.
The shared 'GeneratorHost' takes 'Action<string> log' as a parameter (instead of pulling in a ConsoleAppFramework dependency on the shared library), letting consumers pass their existing 'ConsoleApp.Log' method group.
Validated end-to-end for all 5 generators: ref-gen 8/8 files 0 diffs, WinMD 5632/5632 bytes 16-byte (MVID-only) delta, projection 327/327 files 0 diffs, impl 257536/257536 bytes 0 byte diffs, interop smoke-tested. All 5 build clean (0 warnings) in Release and AOT-publish cleanly for win-arm64.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move shared generator infrastructure into a core project. Several source files were relocated from src/WinRT.Generator.Cli/* to src/WinRT.Generator.Core/*, the project file was renamed and its metadata updated (Description, AssemblyTitle, RootNamespace, removal of AssemblyName), dependent generator project references were updated to point at WinRT.Generator.Core.csproj, and the solution file was adjusted to reference the new project path.
Refactor namespaces and usings: replace WindowsRuntime.GeneratorCli.* with WindowsRuntime.Generator.* (and related sub-namespaces like Attributes, DebugRepro, Errors, Helpers, Parsing, Extensions) across the core library and all generator projects (Impl, Interop, Projection, ReferenceProjection, WinMD). This unifies naming/imports across many files; no behavioral logic changes were made.
Replace explicit XML <summary> comments with <inheritdoc/> on the Token and DebugReproDirectory properties to inherit documentation and reduce duplication. Applied to generator argument classes: ImplGeneratorArgs, InteropGeneratorArgs, ProjectionGeneratorArgs, ReferenceProjectionGeneratorArgs, and WinMDGeneratorArgs (five files).
…types
Each of the 5 generator '*Args' types had three thin one-liner wrappers
('ParseFromResponseFile(string)', 'ParseFromResponseFile(Stream)',
'FormatToResponseFile()') that forwarded straight to the shared
'ResponseFileParser.Parse<TArgs, TErr>' and 'ResponseFileBuilder.Format'
APIs. They contributed more XML doc lines than actual code, with no
behavioral value beyond closure over the per-tool 'WellKnown*Exceptions'
factory.
Delete the 15 wrappers (3 per args type) and inline all call sites:
* '*Generator.cs' 'parseFromResponseFile:' callback now uses the parser
method group directly ('ResponseFileParser.Parse<XArgs, WellKnownXExceptions>')
instead of forwarding through the args type.
* '*Generator.DebugRepro.cs' stream-parse call sites use the same parser
method directly, and format call sites use 'ResponseFileBuilder.Format'
directly.
* Unused 'System.IO', 'WindowsRuntime.Generator.Parsing' and per-tool
'Errors' usings removed from each '*Args.cs'; the parsing using is
added to the DebugRepro and Generator files that now need it.
* Repoint the broken XML cref in 'WinMDGenerator' and update the MSBuild
comment in 'WinRT.Internal.csproj' that named the deleted method.
Net diff: 15 files, +52 / -207 LOC. All 5 generators still build with
zero warnings (Release) and pass end-to-end debug-repro validation
(ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen:
327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'UnhandledGeneratorException' had a 'QuotePhaseInMessage' virtual hook defaulting to false, overridden to true only by 'UnhandledInteropException', which forced the 'ToString' implementation to first format the phase via a conditional and then build the final message by concatenating four '$"""..."""' raw interpolated string fragments. The behavioral discrepancy between tools was almost certainly an accident (both pieces of code predate the shared base class). Remove the hook and always wrap the phase name in single quotes for every generator. With the conditional gone, the entire message collapses into a single regular interpolated string and the override is no longer needed. All 5 generators build clean and pass debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'WinRT.Impl.Generator' and 'WinRT.Projection.Generator' each ship a copy of 'SignatureComparerExtensions' that has zero call sites in their own project. The real consumer is 'WinRT.Interop.Generator', which has its own (larger, strict-superset) copy at 'src/WinRT.Interop.Generator/Extensions/SignatureComparerExtensions.cs'. The two dead copies appear to have been left behind during earlier refactors. Delete them. No behavioral change is possible since they had no callers; the Interop copy is untouched and continues to serve its local consumers. Net diff: -141 LOC across 2 files. Both projects build with 0 warnings in Release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend 'DebugReproPacker' with three orchestration helpers that capture
the preamble/tail boilerplate that all 5 generators' 'SaveDebugRepro'
and 'UnpackDebugRepro' methods used to repeat verbatim:
* 'BeginSave<TError>(directory, toolName, archiveFileName)' validates
the user-provided directory exists (throws the per-tool
'TError.DebugReproDirectoryDoesNotExist' via the shared
'IGeneratorErrorFactory' contract), builds the target '.zip' path
and creates a fresh '{toolName}-debug-repro-{Guid}' staging dir.
* 'FinalizeSave(tempDirectory, zipPath)' deletes any pre-existing
archive, zips the staging dir, and deletes the staging dir.
* 'CreateUnpackTempDirectory(toolName)' creates a fresh
'{toolName}-debug-repro-unpack-{Guid}' directory.
Each '*Generator.DebugRepro.cs' now calls these helpers in place of
the inlined boilerplate. Per-tool concerns stay per-tool: subdirectory
layout, file-copy strategy, '.rsp' build, and JSON path-map writes
are all unchanged. Per-tool exception identity is fully preserved
because every 'BeginSave<TError>' call binds to the tool's own
'WellKnown*Exceptions' static-abstract factory.
Net diff: 6 files, +103 / -140 LOC (-37 LOC). All 5 generators build
clean and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs,
WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen:
0 byte diffs, interop-gen: smoke OK).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'Impl', 'Interop', and 'WinMD' each shipped their own copy of 'RuntimeContextExtensions.LoadModule', with bodies that are character-for-character identical (only the receiver parameter name differed). Impl had only the 'PEImage' overload, Interop had only the 'string' overload, WinMD had both. Consolidate to a single file under 'WinRT.Generator.Core' that carries both overloads. All three consumer projects pick up the overload they already used; no behavioral change. This adds an 'AsmResolver.DotNet' package reference to 'WinRT.Generator.Core' (all 5 generators already reference AsmResolver directly). Net diff: 6 files, +63 / -126 LOC (-63 LOC). All 5 generators build with 0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Every generator's 'Run' method historically opens with a single
'GeneratorHost.Prepare<TArgs>' call (which handles the shared
unpack/parse/save preamble) and then proceeds through a series of
phases (discovery, processing, emit, ...). Each phase was wrapped in
an identical:
try
{
ConsoleApp.Log("...");
DoPhase(args);
}
catch (Exception e) when (!e.IsWellKnown)
{
throw new UnhandledXxxException("phase-name", e);
}
That is now expressed as a single 'runner.RunPhase' call. The
'GeneratorPhaseRunner' readonly struct captures the per-tool
'wrapUnhandled' + 'log' delegates once (it is returned bound to them
by 'GeneratorHost.Prepare') and offers four overloads ('Action' /
'Func<T>', each with or without a log message). All four invariably
route through the per-tool 'wrapUnhandled' delegate so each tool
keeps throwing its own 'UnhandledXxxException' with the right phase
name.
'GeneratorHost.Prepare<TArgs>' now returns '(TArgs Args,
GeneratorPhaseRunner Runner)' instead of just 'TArgs'. The 5 'Run'
methods deconstruct the tuple and use the runner for each phase.
Impl's 'LoadOutputModule(args, out runtimeContext, out outputModule)'
becomes a tuple-return 'LoadOutputModule(args) -> (RuntimeContext,
ModuleDefinition)' so it fits the 'Func<T>' overload cleanly.
'ThrowIfCancellationRequested' calls stay per-tool at the call sites
(the pattern isn't uniform: some phases use 'args.Token', WinMD uses
'token', and the last phase typically has no check). ReferenceProjection's
second phase wraps to a well-known 'CsWinRTProcessError' rather than
the 'Unhandled' factory, so it's left as an inline try/catch.
Net diff across modified files: -84 LOC. Including the new
'GeneratorPhaseRunner.cs' (~123 LOC, mostly XML docs), the overall
file diff is +39 LOC but with a much lower density of error-handling
boilerplate at the per-tool call sites. All 5 generators build with
0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8
0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs,
impl-gen: 0 byte diffs, interop-gen: smoke OK).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…orMessages
Each per-tool 'WellKnown*Exceptions' factory implements the same 6
logical errors from 'IGeneratorErrorFactory' with byte-identical
message text (5 of 6 strings are character-for-character identical
across all 5 tools; the sixth, 'ResponseFileReadError', only varies
by the embedded tool name).
Move the message templates to a new shared 'WellKnownGeneratorMessages'
class in 'WinRT.Generator.Core' and have each per-tool factory call
through to it. Per-tool error ID prefixes (e.g. 'CSWINRTIMPLGEN0001'
vs 'CSWINRTINTEROPGEN0028'), per-tool concrete exception types
('WellKnownImplException' / 'WellKnownInteropException' / ...) and
per-tool numeric IDs are all unchanged.
While here, replace the per-tool '<summary>' doc on each of these 6
methods with '<inheritdoc cref="IGeneratorErrorFactory.X(...)"/>'. The
per-tool summaries were byte-identical to the interface summaries, so
this is no info loss; it also avoids future drift.
Net diff: 6 files (5 modified + 1 new), -60 LOC across the per-tool
factories and +70 LOC for the new central messages file. The real win
is the divergence-protection: any future message tweak now lives in
one place. All 5 generators build clean and pass end-to-end debug-repro
validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta,
proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both Impl and Interop shipped their own 'MvidGenerator' with
complementary overloads ('CreateMvid(Guid, Guid)' for Impl;
'CreateMvid(params IEnumerable<string>)' for Interop). Interop's
overload also depended on a local 'IncrementalHashExtensions.AppendData(Stream)'
extension whose only consumer was that one method.
Move both 'MvidGenerator' overloads into a single file in 'WinRT.Generator.Core'
and relocate 'IncrementalHashExtensions' alongside it. Each consumer continues
to call the exact overload it already used, with no behavioral change. The
impl-gen end-to-end output is byte-identical (257536 bytes, 0 byte diffs)
which proves the 'Guid+Guid' MVID computation is preserved exactly.
Net diff: 5 files changed, 2 new + 3 deleted. All 5 generators build with
0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs,
WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen:
0 byte diffs, interop-gen: smoke OK).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 160-byte CsWinRT 3.0 strong-name public keys lived in several
inconsistent places across the generators:
* 'ImplValues.PublicKeyData' (Impl) - the SDK projection key, used 3x,
but the type/property name suggested it was a generic 'PublicKey'.
* 'InteropValues.WindowsSdkProjectionPublicKey[Data]' (Interop) -
the same SDK projection key, dead code (0 callers).
* 'InteropValues.CsWinRTPublicKey[Data]' (Interop) - the real CsWinRT
runtime key, used 1x.
* 'ProjectionGenerator.Emit.CsWinRTPublicKey' (Projection) - the SDK
projection key again, used 1x, but misnamed as the CsWinRT key.
The const-string forms ('PublicKey', 'CsWinRTPublicKey',
'WindowsSdkProjectionPublicKey') had 0 callers anywhere - leftover
dead code. The same 'B5FC90E7...' byte sequence appeared 3 times,
which is exactly the kind of duplication that risks accidental drift.
Consolidate into a single 'WellKnownPublicKeys' in 'WinRT.Generator.Core':
* 'WindowsSdkProjection' - the precompiled SDK projection assembly key
(used by Impl forwarder refs and by Projection's Roslyn delay-signing).
* 'CsWinRT' - the real CsWinRT 3.0 runtime release key (used by Interop
when emitting refs to 'WinRT.Runtime.dll').
Each live caller is repointed; Projection wraps the 'byte[]' as
'ImmutableArray<byte>' on the fly via 'ImmutableCollectionsMarshal'
for Roslyn's API. The dead-code declarations are deleted, including
the now-empty 'ImplValues' and 'InteropValues' files entirely.
The public key tokens ('Interop\WellKnownPublicKeyTokens.cs' with
6 entries + 'WinMD\WellKnownPublicKeyTokens.cs' with 1 entry) are
unioned into 'WinRT.Generator.Core\References\WellKnownPublicKeyTokens.cs'
(7 entries total: MSCorLib + the 6 from Interop).
Net diff: 10 modified files + 2 new + 4 deleted (16 files total),
-22 LOC. Removes the dead 'WindowsSdkProjectionPublicKey[Data]'
declarations from 'InteropValues' (~31 LOC), fixes the
'PublicKey'/'CsWinRTPublicKey'/'WindowsSdkProjection' naming
inconsistency, and centralizes the canonical byte sequences in one
file. The byte-identical impl-gen output is the strongest possible
confirmation that the consolidated keys match the originals exactly.
All 5 generators build with 0 warnings and pass end-to-end debug-repro
validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta,
proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'GeneratorHost.Prepare<TArgs>' previously returned a tuple
'(TArgs Args, GeneratorPhaseRunner Runner)', and each per-tool 'Run'
method captured the 'args' local in a closure for every phase body
('runner.RunPhase("phase", () => DoPhase(args, ...))').
Make 'GeneratorPhaseRunner' generic in 'TArgs', have it capture the
parsed args directly, and pass them to every body delegate. The
'RunPhase' overloads now take 'Action<TArgs>' / 'Func<TArgs, T>'
instead of 'Action' / 'Func<T>'. Per-tool exception identity is fully
preserved because the per-tool 'wrapUnhandled' delegate is still
invoked unchanged.
Phase bodies that only need 'args' now bind to a method group (e.g.
'body: Discover' instead of 'body: () => Discover(args)') and allocate
zero closures per invocation. Five phases across the five generators
benefit from this: WinMD 'Discover', Interop 'Discover', Projection
'ProcessReferences', ProjectionRef 'BuildWriterOptions', and Impl
'LoadOutputModule'. The remaining eight phases (which need additional
state besides 'args') still use lambdas, but with 'args' as an
explicit parameter rather than a captured local.
Rename 'GeneratorHost.Prepare<TArgs>' to 'GeneratorHost.CreateRunner<TArgs>'
since the method now exclusively returns the runner (no longer a
preamble-only operation that needs a separate "prepare" verb).
Net diff: 7 files, +83 / -67 LOC. All 5 generators build with
0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8
0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs,
impl-gen: 0 byte diffs, interop-gen: smoke OK).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor response-file parsing and related generator messaging: - Consolidate and simplify WellKnownGeneratorMessages (make ResponseFileReadError a constant and clean up XML docs) and update per-tool exception factories to use the new message form. - Restructure ResponseFileParser: split line parsing into a map builder and a Populate method, adjust DynamicallyAccessedMembers annotations, remove nullable-inspection/NullabilityInfo usage, and simplify default-application logic. - Tidy ResponseFileBuilder formatting and annotations; minor comment and formatting cleanups. - Remove an unused PathExtensions.Normalize(ReadOnlySpan<char>) overload and add small GeneratorHost/GeneratorPhaseRunner code cleanups (target-typed new, pragma for warnings, reorder assignments). These changes reduce duplication, simplify trimming/reflection annotations for the linker, and clarify parsing behavior without changing external behavior.
Rename the UnhandledGeneratorException property from GeneratorDescription to GeneratorName and update the standardized ToString message to use the new name ("The CsWinRT {GeneratorName} generator..."). Adjust all per-tool unhandled exception types to provide GeneratorName values. Remove explicit generic type args from several GeneratorHost.CreateRunner calls (relying on inference). Also apply minor XML-doc and cref cleanups (simplify exception/type cref qualifications and projection writer cref) and a small doc fix in WindowsRuntimeExceptionExtensions.
The 'InternalsVisibleTo' items in 'src/WinRT.Generator.Core/Properties/AssemblyInfo.cs' are emitted by the .NET SDK directly from MSBuild '<InternalsVisibleTo>' items when they're declared in the project file, so there is no need for a hand-written 'AssemblyInfo.cs' to host them. Move the 5 entries (one per consuming generator) into 'WinRT.Generator.Core.csproj' and delete the now-empty 'Properties/AssemblyInfo.cs' (plus its empty parent folder). All 5 generators build with 0 warnings, confirming the SDK-emitted attributes still grant the same internals access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Simplify references by removing unnecessary qualification prefixes: replace System.StringComparison with StringComparison and global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run with ProjectionWriter.ProjectionWriter.Run. This is a code cleanup across generator tasks (RunCsWinRTWinMDGenerator.cs, ProjectionGenerator.Generate.cs, ReferenceProjectionGenerator.cs) with no behavior change.
Convert GeneratorPhaseRunner<TArgs> to use C# primary-constructor syntax, removing explicit private fields and the manual constructor. Replace uses of _wrapUnhandled and _log with the positional parameters wrapUnhandled and log, and make Args return the positional args field. Add XML param docs for the new parameters. This is a refactor to reduce boilerplate without changing behavior.
Every per-tool 'Run' method previously placed a manual
'args.Token.ThrowIfCancellationRequested()' between consecutive
phases - 8 explicit calls across the 5 generators.
Bake that check into 'GeneratorPhaseRunner<TArgs>.RunPhase' (all 4
overloads): after the body completes successfully, the runner calls
'args.Token.ThrowIfCancellationRequested()' before returning. The
check sits OUTSIDE the 'try'/'catch' so the resulting
'OperationCanceledException' propagates without going through the
per-tool 'wrapUnhandled' delegate, which is already a no-op for it
('OperationCanceledException' is in 'IsWellKnown').
Per-tool 'Run' methods drop the 8 redundant
'runner.Args.Token.ThrowIfCancellationRequested()' calls; the last
phase of each generator now does one harmless extra cancellation
check before the success log message.
Net diff: 6 files, +18 / -18 LOC. All 5 generators build with 0 warnings
and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs,
WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen:
0 byte diffs, interop-gen: smoke OK).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename src/WinRT.Generator.Core/GeneratorPhaseRunner.cs to src/WinRT.Generator.Core/GeneratorPhaseRunner{TArgs}.cs. No source changes were made; this update clarifies the file name to reflect the generic TArgs type parameter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extract a new shared
WinRT.Generator.Corelibrary, then route the 5 CsWinRT CLI generators (Impl, Interop, Projection, ProjectionRef, WinMD) through it. The net effect is -1982 LOC across 102 files and a single source of truth for every concern the generators previously duplicated: response-file parsing/formatting, debug-repro packing, error contracts, message templates, MVID hashing, public keys, phase orchestration, etc.Motivation
After the round-1
WinRT.Generator.Coreextraction (6 phases) and the addition of debug-repro support to every generator, several helpers, message strings, public-key declarations and try/catch boilerplate were still duplicated 2-5 times across the per-tool projects. Some cases were outright dead code; others had drifted into naming inconsistencies (the same 160-byte SDK projection strong-name key was declared in 3 different files under 3 different names).The recurring per-phase
try { Log; Body; } catch { wrap as Unhandled }+args.Token.ThrowIfCancellationRequested()boilerplate also dominated everyRunmethod's body. Driving every phase through a sharedGeneratorPhaseRunner<TArgs>collapses each phase to a singlerunner.RunPhasecall while preserving per-tool exception identity exactly (sameUnhandled*Exceptionsubtype, same phase tag, same per-tool factory IDs).The work in this PR was driven by a validated merged report from a multi-agent analysis (5 agents: Opus 4.8, Opus 4.7 1M, Opus 4.6, GPT-5.5, GPT-5.3-Codex).
Changes
Round-1: extract
WinRT.Generator.Core(6 phases)src/WinRT.Generator.Core/(new project): shared CLI infrastructureFileExtensions,PathExtensions,CommandLineArgumentNameAttribute,GeneratorJsonSerializerContext)WellKnownGeneratorException,UnhandledGeneratorException,GeneratorExceptionExtensions)IGeneratorErrorFactoryso per-tool error IDs flow through a single static-abstract contractResponseFileParserandResponseFileBuilder; delete every per-tool hand-rolled response-file parser/formatterDebugReproPacker.GetHashedFileName,CopyHashedFilesToDirectory,CopyHashedFileToDirectory,CopyPathMapToDirectory,ExtractPathMap)GeneratorHost.Prepareto encapsulate the shared unpack → parse → save preambleRound-2: extend the shared core (the 7 candidates from the merged R2 report)
Delete dead SignatureComparerExtensions copiesin Impl + Projection (-141 LOC; only Interop has live consumers).DebugReproPackerorchestration helpers:BeginSave<TError>,FinalizeSave,CreateUnpackTempDirectory. Per-tool exception identity preserved viaIGeneratorErrorFactory.RuntimeContextExtensions.LoadModulemoved to Core with bothstringandPEImageoverloads. AddsAsmResolver.DotNetas a Core package reference.GeneratorPhaseRunner<TArgs>(new generic struct returned byGeneratorHost.CreateRunner): captures the parsed args plus the per-toolwrapUnhandledandlogdelegates, and forwards the args to every body delegate. EveryRunPhaseoverload automatically callsArgs.Token.ThrowIfCancellationRequested()after the body completes successfully.Preparerenamed toCreateRunner.WellKnownGeneratorMessages(new): the 6 message templates for theIGeneratorErrorFactorycontract live in one place. Per-tool factories use<inheritdoc/>for the docs.MvidGenerator+IncrementalHashExtensionsconsolidated in Core (Impl'sGuid+Guidoverload + Interop'sIEnumerable<string>overload).WellKnownPublicKeys(new): the SDK projection strong-name key and the CsWinRT runtime key, each declared once. Fixes the previous naming inconsistency and removes 3 duplicated declarations plusInteropValues.WindowsSdkProjectionPublicKey[Data](which had 0 callers).WellKnownPublicKeyTokens: union of Interop's 6 tokens + WinMD'sMSCorLibtoken in a single Core file.InternalsVisibleTodeclarations for the 5 consumers moved fromAssemblyInfo.csto MSBuild items in the.csproj.Round-3: polish (the 5 user cleanup commits on top)
global::/System.qualifiers.GeneratorPhaseRunner<TArgs>.GeneratorPhaseRunner{TArgs}.cs(with generic-arity suffix).Validation
Every commit was validated end-to-end via the debug-repro round-trip harness:
ref-gen: 8/8 .cs files, 0 diffsWinMD: 5632/5632 bytes, 16-byte MVID-only delta (expected non-determinism)proj-gen: 327/327 .cs files, 0 diffsimpl-gen: 257536/257536 bytes, 0 byte diffsinterop-gen:--helpsmoke test passesAll 5 generators build with 0 warnings (Release) and AOT-publish cleanly for
win-arm64.Net impact
102 files changed, +1659 / -3641 LOC (net -1982 LOC) at the per-tool call sites. The new
WinRT.Generator.Corelibrary hosts:Attributes/CommandLineArgumentNameAttribute.csDebugRepro/DebugReproPacker.cs(leaf helpers + orchestration)Errors/{GeneratorExceptionExtensions, IGeneratorErrorFactory, UnhandledGeneratorException, WellKnownGeneratorException, WellKnownGeneratorMessages}.csExtensions/{FileExtensions, IncrementalHashExtensions, PathExtensions, RuntimeContextExtensions}.csGeneratorHost.cs+GeneratorPhaseRunner{TArgs}.cs+IGeneratorArgs.csHelpers/{GeneratorJsonSerializerContext, MvidGenerator}.csParsing/{ResponseFileBuilder, ResponseFileParser}.csReferences/{WellKnownPublicKeys, WellKnownPublicKeyTokens}.cs