Skip to content

Refactor projection writer: cleanup passes, parallelization, and CI-driven correctness fixes#2418

Open
Sergio0694 wants to merge 229 commits into
staging/CodeWritersfrom
user/sergiopedri/code-writers-refactor
Open

Refactor projection writer: cleanup passes, parallelization, and CI-driven correctness fixes#2418
Sergio0694 wants to merge 229 commits into
staging/CodeWritersfrom
user/sergiopedri/code-writers-refactor

Conversation

@Sergio0694
Copy link
Copy Markdown
Member

Summary

Second PR in the CsWinRT projection-writer migration series. Mostly automated cleanup of the C# port that landed in the previous PR: refactor passes, type-semantics consolidation, parallelization, error-discipline alignment, and a stack of CI-driven semantic + cosmetic fixes. The output of the writer now compiles cleanly against the real Windows SDK and matches the original C++ writer's behavior across all 8 regression scenarios.

Motivation

The previous PR landed the initial C++ -> C# port of cswinrt as WinRT.Projection.Writer. That port was a faithful 1:1 transcription, but the resulting code carried a lot of structural debt from the C++ shape: long mega-functions, ad-hoc helper conventions inherited from C++ string-template emission, defensive null checks the C++ never had, and a few correctness regressions introduced during automated refactors.

This PR is the cleanup pass: refactor toward C# idioms, share helpers across the writer/factories, parallelize the per-namespace work, and fix every semantic + cosmetic regression we surfaced via CI iteration. The goal is to leave the writer in a state where the only remaining work is hand-polish (next PR) and deleting the legacy C++ tree (final PR).

This is PR 2 of 4 in the migration series:

  1. Initial C# port (already merged)
  2. This PR -- automated/structural cleanup + correctness fixes
  3. Upcoming: manual polishing pass + interpolated-string-handler optimization for zero-allocation emit paths
  4. Upcoming: delete the legacy C++ cswinrt tree, the test runner, and other temporary scaffolding

Changes

Resolver and helper consolidation

  • Resolvers/AbiTypeShapeResolver.cs -- centralized ABI type classification (BlittablePrimitive, Enum, BlittableStruct, ComplexStruct, NullableT, GenericInstance, RuntimeClassOrInterface, Delegate, Object, MappedAbiValueType, etc.) so emission paths use a single source of truth instead of inline IsXxx predicates duplicated across factories. Two helper methods were added: IsBlittableStruct (blittable-only) and IsAnyStruct (blittable OR complex), and ~36 call sites were updated to use the right one.
  • Helpers/AbiTypeHelpers.*.cs -- split the legacy AbiTypeHelpers.cs into focused partials (AbiTypeNames, Blittability, etc.).
  • Metadata/TypeSemantics.cs -- moved the TypeSemantics discriminated union out of helpers and trimmed the trailing-underscore record names.

Generation pipeline

  • Generation/ProjectionGenerator.cs + Generation/WorkItems/ -- parallelized projection generation across namespaces, IIDs, and the component-module aggregator via Parallel.ForEach. Cross-item shared state moved into ProjectionGeneratorRunState with concurrent collections.
  • Generation/ProjectionGenerator.GeneratedIids.cs -- pulled GeneratedInterfaceIIDs.cs emission into its own work item; restored proper IncreaseIndent/DecreaseIndent around the per-IID property emission.
  • Generation/ProjectionGenerator.Generate.cs -- fixed an SDK-mode fallback that forgot to set hasTypesToProject = true when no Microsoft.Windows.SDK.NET reference assembly was present (the generator silently produced no output).

Writer infrastructure

  • Writers/IndentedTextWriter.cs -- removed all 4 auto-fixup heuristics (brace-prepend on {/}, auto-trailing-newline on multilines ending in ;/}/]/#, brace-prepend in WriteLine(content), always-on idempotent WriteLine()). The writer now mirrors the source-generator design: callers are responsible for emitting the right newlines, with WriteBlock() providing the canonical brace+indent pattern. Idempotent newline behavior is opt-in via WriteLine(skipIfPresent: true).
  • Writers/IndentedTextWriterPool.cs -- new pool to reduce allocations across parallel work items. Acquired via the disposable IndentedTextWriterOwner ref struct.
  • Helpers/TypedefNameWriter.cs -- restored InAbiNamespace / InAbiImplNamespace tracking that was lost during the refactor; restored 4 missing conditions in the namespace-prefix logic verbatim from the C++ source (code_writers.h:363-369) so interface declarations no longer get fully qualified inside their own ABI namespace.

Error discipline

  • Errors/WellKnownProjectionWriterExceptions.cs -- aligned exception emit with the WinRT.Interop.Generator style: error IDs in the 5000+ range, well-known guards in ProjectionWriter.Run, per-site ThrowOrAttach exception context, no [SuppressMessage] workarounds, Settings made truly read-only via a one-shot MakeReadOnly() pattern.
  • Generation/ProjectionGenerator.Emit.cs + Generation/ProjectionGeneratorArgs.cs -- added MaxDegreesOfParallelism plumbing through the CLI; better cancellation granularity.

Code cleanups

  • All factories under Factories/ -- 50+ refactor passes (commits prefixed R2, R3, F, N, Pass <N>) for consistent code style: single-line ifs expanded with blank-line padding, multi-line WriteLine -> raw multiline strings, scratch-writer string-returning overloads, fully-qualified marshaller call sites, etc.
  • Resources/Additions/Microsoft.UI.Xaml/Microsoft.UI.Xaml.GridLength.cs + Resources/Additions/Windows.UI.Xaml/Windows.UI.Xaml.GridLength.cs -- fixed a Pixe)l typo introduced by the IDE0078 automated refactor (type is not (GridUnitType.Auto or GridUnitType.Pixe)l && ... was supposed to be type is not (GridUnitType.Auto or GridUnitType.Pixel) && ...).
  • Resources/Base/ReferenceInterfaceEntries.cs -- moved using static System.Runtime.InteropServices.ComWrappers; after the normal using block per project convention.

Semantic regressions fixed (surfaced by CI)

The following semantic bugs were introduced during the cleanup passes and caught only by iterating against CI. This is exactly why validate-compile.ps1 was added (see "Validation infrastructure" below).

  • Nullable getter ABI marshalling -- returnIsRefType was missing AbiTypeShapeKind.NullableT, so any Nullable<T> getter (e.g. Wallet.ExpirationDate) emitted int __retval + a direct cast instead of void* __retval + try/finally + Marshaller.UnboxToManaged.
  • Enum array (EnumType[]) returns -- IsBlittablePrimitive excluded the Enum shape, so returnIsReceiveArray failed for enum-element arrays. Span<EnumType> FillArray emitted CopyToManaged_X referencing nonexistent ABI.Foo.EnumName types.
  • Complex struct returns -- returnIsAnyStruct covered both BlittableStruct AND ComplexStruct shapes, so the dispatch never reached the returnIsComplexStruct branch. ComplexStruct returns (e.g. GpioChangeCount, GpioChangeRecord, AccessListEntry, StorePackageUpdateStatus) emitted the projected struct as __retval instead of the ABI struct, with no ConvertToManaged wrap.
  • ProfileUsageMarshaller missing methods -- StructEnumMarshallerFactory.almostBlittable was wrongly true for complex structs, so isComplexStruct became false and the marshaller class skipped emitting ConvertToUnmanaged / ConvertToManaged / Dispose.
  • source: typeof(Foo) line split -- the IDIC TypeMapAssociation emitter wrapped typeof( with a trailing newline, splitting the typedef name across lines.
  • Comma-on-own-line in vtable args -- multiline WriteLine in the PassArray/Out/Ref/ReceiveArray loops appended a trailing newline so the next iteration's leading , landed at start-of-line and got an indent prepended.
  • Interface declaration mangling in component mode -- TypedefNameWriter dropped 3 conditions from the C++ namespace-prefix logic, causing internal interface global::ABI.Impl.AuthoringTest.IBasicClassClass (CS1514/CS1513/CS1001).
  • ABI block type-name resolution (InAbiNamespace) -- the writer was emitting projected-namespace type names without the global:: prefix when inside the corresponding ABI namespace, causing CS0246 / CS9051 (file-local type misuse) on LearningModelPixelRange and similar generic interface types.
  • WriteStruct body indent + WriteFileHeader trailing newline + WriteDefault/ExclusiveToInterfaces patterns -- 5+ structural emit sites refactored to use the WriteBlock() pattern instead of flat raw strings without IncreaseIndent.

Cosmetic regressions fixed

  • R1 (ABI delegate Invoke wrapper indent), R2 (XAML CCW marshaller IID accessor), R3 (struct X{ brace packing in Vftbl), R5 (GeneratedInterfaceIIDs.cs member indent), R6 (extra blank line after namespace X {) -- detailed in commit f6dc5485. All produce valid C# but were ugly. R4 (long-line wrap from intentional fully-qualified marshaller names) was accepted as the cost of the safety improvement.

Validation infrastructure

The previous validation harness only diffed PRE writer output against POST. That missed several classes of bug:

  • Embedded resource files copied verbatim into the projection (the Pixe)l typo had matching PRE/POST diffs because the typo lived in the resource).
  • Semantic regressions in marshalling code (the writer happily produces "different but still self-consistent" malformed code).
  • Syntax errors that need binding context to surface.

Two additions:

  • MaxDegreesOfParallelism plumbing through cswinrtprojectiongen.exe CLI -- so deterministic builds and reduced-parallelism debug runs work the same as the interop generator.
  • Real CI-style compile coverage (kept in session state, not committed) -- end-to-end build of WinRT.Sdk.Projection + WinRT.Sdk.Xaml.Projection against the actual Windows 10.0.26100 SDK winmds, plus syntax-only validation across all 8 regression scenarios. Catches CS1xxx parse errors and CS9051 file-local-type errors. Verified by reverting each fix temporarily and confirming the script flags the original CI error.

Validation

  • All 8 regression scenarios produce expected output (244 / 1553 byte-identical vs pre-refactor truth; remaining diffs are the intentional cosmetic improvements: fully-qualified marshaller names, colon-spacing, multi-line <summary>, [FlagsAttribute] -> [Flags], blank-line consistency).
  • WinRT.Sdk.Projection and WinRT.Sdk.Xaml.Projection compile cleanly against the real Windows SDK.
  • WinRT.Projection.Writer builds with 0 warnings under TreatWarningsAsErrors.
  • WinRT.Projection.Generator publishes to Native AOT with 0 warnings.
  • CI is green.

Coming next

  • PR 3 (next): manual polishing pass plus a switch to interpolated string handlers in the hot emit paths to eliminate intermediate StringBuilder/string.Format allocations.
  • PR 4 (final): delete the C++ cswinrt tree under src/cswinrt/, the WinRT.Projection.Writer.TestRunner scaffolding, and any other temporary scaffolding that exists only to bridge the migration.

Sergio0694 and others added 30 commits May 9, 2026 03:28
Adds a `rsp <path>` argument mode to the TestRunner program that parses a
projection-generator response file and invokes `ProjectionWriter.Run` with
the matching options. Used by the refactor PR's per-commit byte-identity
validation script (see session-state `validate-writer-output.ps1`) to drive
the writer against the 8 canonical regen scenarios:

  refgen-truth-full              (the 5 standard regen scenarios from
  refgen-windows                  the original C# port PR validation)
  refgen-everything
  refgen-everything-with-ui
  refgen-pushnot
  refgen-truth-authoring-aligned (input-aligned scenarios used by the
  refgen-truth-winsdk-aligned     truth-comparison harness for byte-for-byte
  refgen-truth-winui-fast         diffing against the C++ tool's output)

The TestRunner project itself will be deleted in Pass 22 (final structural
polish), so this addition is throw-away infrastructure scoped to the refactor.

Captured baseline manifests for all 8 scenarios (1,553 generated `.cs` files
total, SHA256 per file). Every subsequent commit in the refactor PR must
produce byte-identical output across all 8 scenarios; the only exception is
Pass 16 (output-format cleanup) which intentionally changes whitespace and
uses a separate Roslyn parse-equivalence gate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Writer

Drops the redundant `Generator` token from the writer project name. The
writer is a writer, not a generator -- there's already a separate
`WinRT.Projection.Generator` (the orchestrator) and `WinRT.Projection.Ref.Generator`
(the CLI host).

Renames:
- Folder: `src/WinRT.Projection.Generator.Writer/` -> `src/WinRT.Projection.Writer/`
- Project: `WinRT.Projection.Generator.Writer.csproj` -> `WinRT.Projection.Writer.csproj`
- TestRunner folder + project: same pattern
- Root namespace: `WindowsRuntime.ProjectionGenerator.Writer` -> `WindowsRuntime.ProjectionWriter`

External references updated:
- `src/cswinrt.slnx` solution entry + 8 BuildDependency references
- `src/WinRT.Projection.Generator/WinRT.Projection.Generator.csproj` ProjectReference
- `src/WinRT.Projection.Ref.Generator/WinRT.Projection.Ref.Generator.csproj` ProjectReference
- All `using WindowsRuntime.ProjectionGenerator.Writer*;` directives across consumers

Two consumer-side fixups required by the rename:
1. `ProjectionGeneratorProcessingState.cs` previously used the relative
   `Writer.ProjectionWriterOptions` form (relying on `WindowsRuntime.ProjectionGenerator.Writer`
   being a sibling namespace lookup). Added an explicit
   `using WindowsRuntime.ProjectionWriter;` and switched to unqualified `ProjectionWriterOptions`.
2. The new namespace `WindowsRuntime.ProjectionWriter` collides with the static
   class `ProjectionWriter` defined inside it, so `ProjectionWriter.Run(...)` calls
   in consumer code became ambiguous. Fully-qualified the two call sites
   (`ProjectionGenerator.Generate.cs:71`, `ReferenceProjectionGenerator.cs:67`)
   and the matching `<see cref>` doc comments to
   `global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run`.

Validation: all 8 regen scenarios produce byte-identical output to the
captured baseline (see Pass 0 harness).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In CsWinRT 3.0, `NetstandardCompat` is always `false` and the surrounding
infrastructure has long been removed. Drop:

- `Settings.NetstandardCompat` property (`Helpers/Settings.cs`)
- The `!settings.NetstandardCompat` gate in `IsFastAbiClass`. The method
  no longer needs a `Settings` argument; simplified signature is
  `IsFastAbiClass(TypeDefinition type)`. Updated 3 callers in
  `CodeWriters.ClassMembers.cs` and `CodeWriters.ObjRefs.cs`.
- The `s_emptySettingsForFastAbi` workaround field and the
  `private static Settings GetSettings(TypeDefinition _)` helper that
  existed solely to feed an empty Settings into `IsFastAbiClass`.
- The `if (!Settings.NetstandardCompat) { ... }` guards around the
  `#pragma warning disable/restore CA1416` emission in
  `WriteBeginAbiNamespace`/`WriteEndAbiNamespace` (`Writers/TypeWriter.cs`).
  These pragmas are now emitted unconditionally, matching production behavior.

Validation: all 8 regen scenarios produce byte-identical output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The orchestrator used to emit a `ProjectionGenerator.rsp` file alongside
each generation as a "debug artifact" in the historical `cswinrt.exe` CLI
format. This was needed back when the orchestrator launched the C++
`cswinrt.exe` as a child process. The writer is now an in-proc library
called directly via `ProjectionWriter.Run(options)`, so the .rsp file is
purely a historical leftover with no consumers.

Removed:
- `out string rspFile` parameter from `BuildWriterOptions`
- `rspFile = Path.Combine(outputFolder, "ProjectionGenerator.rsp")` line
- `using StreamWriter fileStream = new(rspFile)` declaration
- All 12 `fileStream.WriteLine(...)` calls scattered across the type
  enumeration loops, the `-target`/`-input`/`-output`/`-component`/`-exclude`
  emission lines, and the per-input-WinMD `-input` lines
- `WriteWindowsSdkFilters(StreamWriter writer, List<string>, List<string>, bool)`
  signature: dropped the `StreamWriter writer` parameter; the function is
  now pure list-building. Local `Include`/`Exclude` lambdas simplified to
  expression-bodied form (`includes.Add(ns)` / `excludes.Add(ns)`).
- `rspFilePath` constructor parameter and `RspFilePath` property on
  `ProjectionGeneratorProcessingState`. Updated `ProcessReferences` to
  drop the now-unused `rspFile` destructuring and ctor argument.
- XML doc comments referencing the .rsp debug artifact.

Net result: the orchestrator no longer produces `ProjectionGenerator.rsp`
files and ~30 lines of dead code are gone.

Validation: all 8 regen scenarios produce byte-identical output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reorganize the 16 `CodeWriters.X.cs` partials into role-based folders that
mirror `WinRT.Interop.Generator/`:

- `Builders/`    : top-level orchestrator dispatch (`CodeWriters.cs`)
- `Factories/`   : per-concept emitters (Abi, Class, ClassMembers, Component,
                   Constructors, CustomAttributes, Interface, MappedInterfaceStubs,
                   Methods, ObjRefs, RefModeStubs)
- `Helpers/`     : cross-cutting name/IID computation (Guids, Helpers,
                   InteropTypeName, TypeNames)
- `Writers/`     : kept for the indented-text-writer infrastructure
                   (`TextWriter.cs`, `TypeWriter.cs`)

The moved files all retain their `partial class CodeWriters` declaration in
the root namespace `WindowsRuntime.ProjectionWriter`. No type renames yet
(those happen in Pass 13 when partials get promoted to dedicated
`*Factory`/`*Builder` types). Sub-namespaces will start being introduced in
Pass 5 (Models/) and Pass 6 (Extensions/).

Move-only commit: builds clean, output is byte-identical to baseline across
all 8 regen scenarios.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract data carriers from the catch-all `Helpers/Helpers.cs` and the
`Factories/CodeWriters.ClassMembers.cs` / `Factories/CodeWriters.Class.cs`
god-files into a dedicated `Models/` folder under a sub-namespace
`WindowsRuntime.ProjectionWriter.Models` (matching the interop generator's
convention -- see `WinRT.Interop.Generator/Models/`).

New files:
- `Models/MethodSignatureInfo.cs` (contains `MethodSig` -- type rename deferred to Pass 13)
- `Models/ParameterInfo.cs` (contains `ParamInfo` record)
- `Models/ParameterCategory.cs` (contains `ParamCategory` enum + `ParamHelpers` static class)
- `Models/PropertyAccessorState.cs` (carved out of `CodeWriters.ClassMembers`)
- `Models/StaticPropertyAccessorState.cs` (carved out of `CodeWriters.Class`)

The original definitions are removed from their old locations. Each
consumer file gets a `using WindowsRuntime.ProjectionWriter.Models;`
directive (`Builders/CodeWriters.cs`, plus `Factories/CodeWriters.{Abi,
Class, ClassMembers, Constructors, Interface, Methods}.cs`).

Type names are kept verbatim ("Names stay the same initially; renames happen
in Pass 13" per the refactor plan).

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lper

Introduce the `Extensions/` folder (matching `WinRT.Interop.Generator/Extensions/`)
under sub-namespace `WindowsRuntime.ProjectionWriter.Extensions`. First
extension class is `ITypeDefOrRefExtensions` with a single `Names()` helper
that returns `(string Namespace, string Name)` with both fields guaranteed
non-null (a missing namespace becomes `string.Empty`, a missing name becomes
`string.Empty`).

Migrated 60 occurrences of the 2-line idiom

    string ns = X.Namespace?.Value ?? string.Empty;
    string name = X.Name?.Value ?? string.Empty;

to the equivalent

    (string ns, string name) = X.Names();

across 14 files (Factories/, Helpers/, Generation/, Metadata/). The
extension is defined on `ITypeDefOrRef` (the common base interface of
`TypeDefinition`, `TypeReference`, and `TypeSpecification` in AsmResolver),
so a single extension class covers all three concrete types.

Each migrated file gained a `using WindowsRuntime.ProjectionWriter.Extensions;`
directive.

This pass only handles the 2-line `(ns, name) =` form. Single-field accesses
(`Namespace?.Value` alone, or with `?? ""` fallback variations) and any
remaining inline forms are not touched here -- the goal is mechanical
high-confidence batch migration. Sub-pass 6.2+ will continue with other
extensions (TypeSignature, CustomAttribute, MethodDefinition, etc.) and
remaining call patterns.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e/GetAttribute)

Add `HasAttribute(this IHasCustomAttribute, string ns, string name)` and
`GetAttribute(this IHasCustomAttribute, string ns, string name)` extension
methods on AsmResolver's `IHasCustomAttribute` interface (the common base
for `TypeDefinition`, `MethodDefinition`, `PropertyDefinition`,
`InterfaceImplementation`, etc.). The implementations are inlined from the
existing `TypeCategorization.HasAttribute`/`GetAttribute` static helpers.

Migrated 5 files (CodeWriters.Class.cs, CodeWriters.ClassMembers.cs,
CodeWriters.ObjRefs.cs, ProjectionGenerator.cs, CodeWriters.Guids.cs)
from the static-call form

    TypeCategorization.HasAttribute(member, "Windows.Foundation.Metadata", "DefaultAttribute")
    Helpers.HasAttribute(impl, "Windows.Foundation.Metadata", "OverridableAttribute")

to the extension-method form

    member.HasAttribute("Windows.Foundation.Metadata", "DefaultAttribute")
    impl.HasAttribute("Windows.Foundation.Metadata", "OverridableAttribute")

The original static `TypeCategorization.HasAttribute`/`GetAttribute` and
the `Helpers.HasAttribute`/`GetAttribute` forwarders are kept for now;
they will be removed in Pass 8 when the catch-all Helpers.cs is split.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…efinition extensions

Convert the static helpers in `Helpers/Helpers.cs` that wrap AsmResolver
metadata APIs into proper extension methods organized per-receiver-type:

- `Extensions/MethodDefinitionExtensions.cs`:
  `IsConstructor`, `IsSpecial`, `IsRemoveOverload`, `IsNoExcept`
- `Extensions/PropertyDefinitionExtensions.cs`:
  `IsNoExcept`
- `Extensions/InterfaceImplementationExtensions.cs`:
  `IsDefaultInterface`, `IsOverridable`
- `Extensions/TypeDefinitionExtensions.cs`:
  `GetDefaultInterface`, `GetDelegateInvoke`, `HasDefaultConstructor`

Migrated all call sites of `Helpers.IsX(member)` to the equivalent
extension method form `member.IsX()` across 9 files
(Builders/CodeWriters.cs, Factories/CodeWriters.{Abi,Class,ClassMembers,
Component,Constructors,Interface}.cs, Helpers/CodeWriters.{Guids,Helpers}.cs).

The original static helpers in `Helpers/Helpers.cs` are kept for now as
forwarders to the extensions; they will be removed in Pass 8 when the
catch-all Helpers.cs is split.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Decompose `Generation/ProjectionGenerator.cs` (413 lines) into focused
partial-class files mirroring the interop generator's `InteropGenerator.X.cs`
decomposition convention:

- `ProjectionGenerator.cs` (top-level shell + `Run()` orchestration -- now 196 lines)
- `ProjectionGenerator.Namespace.cs` (the `ProcessNamespace` helper that
  emits a single namespace's projection .cs file -- 4-phase pipeline:
  TypeMapGroup attributes, projected types, ABI types, custom additions)
- `ProjectionGenerator.Resources.cs` (the `WriteBaseStrings` helper that
  emits the embedded `Resources/Base/*.cs` resources verbatim into the
  output folder, with the auto-generated header prepended)

Made `ProjectionGenerator` a `partial class`. No method bodies changed;
this is a pure file-level move + partial-class declaration. The IID
writing block and the component activatable discovery remain inline in
`Run()` for now -- extracting them would require carving out the inline
state held by `Run()` (factory interface tracking, the GuidWriter handle,
etc.). They are appropriate candidates for `ProjectionGenerator.GeneratedIids.cs`
and `ProjectionGenerator.Component.cs` partials in a follow-up.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r-backed)

Add `Writers/IndentedTextWriter.cs` -- a clean general-purpose indented-text
writer that is *only* about writing text. WinRT-specific concerns (file
headers, projection namespace blocks, mode flags, metadata cache) will be
split out of `TextWriter`/`TypeWriter` in Pass 10 and live as extension
methods or on the new `ProjectionEmitContext` type.

This implementation is a `class` backed by a `StringBuilder` so it can be
passed around freely and live as long as needed in a long-running tool.
The source generator's variant
(`src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs`) is
a `ref struct` backed by a `DefaultInterpolatedStringHandler` because it
lives in a Roslyn source generator where transient compilation context +
zero-alloc are the priority. The `class` variant trades a bit of perf for
ergonomics that match the writer project's needs.

API surface:
- `IncreaseIndent()` / `DecreaseIndent()`
- `Block WriteBlock()` returning a `struct Block : IDisposable` for
  `using (writer.WriteBlock()) { ... }` scoped emission of `{ ... }` blocks
- `Write(string, bool isMultiline = false)` and
  `Write(ReadOnlySpan<char>, bool isMultiline = false)`
- `WriteLine()`, `WriteLine(string, bool)`, `WriteLine(ReadOnlySpan<char>, bool)`
- `WriteIf(bool, ...)`, `WriteLineIf(bool, ...)`
- `ToStringAndClear()`

Per the v5 refactor plan, this initial revision intentionally does NOT
include custom interpolated-string handlers (the perf-focused
`InterpolatedStringHandlerArgument` overloads). Callsite syntax is
identical (`writer.Write($"...")`) against either the `string` overload
(via interpolation) or a future handler-based overload, so adding handlers
later is a non-breaking optimization that does not require any callsite
churn. The user will add specialized handlers later as a perf follow-up.

Indentation is per-line: the first `Write` after a newline (or at buffer
start) prepends the current indentation; mid-line writes do not. Multiline
content normalizes `CRLF` -> `LF` and indents each line via the current
indentation level. Empty lines never receive indentation, so raw
multi-line literals with blank lines do not gain trailing whitespace.

`WriteLine(skipIfPresent: true)` collapses runs of blank lines and
suppresses a blank line immediately after `{` -- foundational support for
Pass 16's blank-line-collapsing rule.

This commit is purely additive: no existing call sites change yet.
The old `TextWriter` and `TypeWriter` continue to be used. Output is
byte-identical to baseline across all 8 regen scenarios.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sed files

Decompose `Helpers/Helpers.cs` (originally 184 lines, mix of unrelated
concerns) into focused single-purpose helpers/extensions:

- `Helpers/CSharpKeywords.cs` -- the C# keyword `HashSet` + `IsKeyword(string)`
- `Helpers/IdentifierEscaping.cs` -- `StripBackticks`, `WriteEscapedIdentifier`
- `Helpers/AccessibilityHelper.cs` -- `InternalAccessibility(Settings)`
- Moved to `Extensions/PropertyDefinitionExtensions.cs`: `GetPropertyMethods`
- New `Extensions/EventDefinitionExtensions.cs`: `GetEventMethods`
- Moved to `Extensions/TypeDefinitionExtensions.cs`: `GetContractVersion`,
  `GetVersion`

Migrated all call sites across 10 files (Builders, Factories, Helpers).
The remaining `Helpers/Helpers.cs` (62 lines, down from 184) contains only
`GetExclusiveToType(TypeDefinition iface, MetadataCache cache)` -- the one
helper that genuinely needs the `MetadataCache` (and so cannot be expressed
as a pure extension method).

The dead static forwarders that were kept after Pass 6 (HasAttribute,
GetAttribute, IsConstructor, IsSpecial, IsRemoveOverload, IsNoExcept,
IsDefaultInterface, IsOverridable, GetDefaultInterface, GetDelegateInvoke,
HasDefaultConstructor) are now removed: every consumer has been migrated
to the corresponding extension method on the appropriate AsmResolver type.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…edicates)

Complete Pass 6 with the remaining cache-free TypeSignature shape predicates:

- `IsString()` -- corlib `System.String` check
- `IsObject()` -- corlib `System.Object` check
- `IsSystemType()` -- `System.Type` / `Windows.UI.Xaml.Interop.TypeName`
- `IsNullableT()` -- generic instance of `IReference<T>` or `Nullable<T>`
- `GetNullableInnerType()` -- the `T` of a `Nullable<T>`-shaped instance
- `IsGenericInstance()` -- any generic instance signature
- `IsHResultException()` -- `System.Exception` / `Windows.Foundation.HResult`
- `StripByRefAndCustomModifiers()` -- peel byref + modreq/modopt wrappers
- `IsByRefType()` -- byref check that peels custom modifiers first

Migrated 156 call sites across `Factories/CodeWriters.Abi.cs` (135) and
`Factories/CodeWriters.Constructors.cs` (21). Removed the now-unused
private `IsString`/`IsObject`/`IsSystemType`/`IsNullableT`/`GetNullableInnerType`/
`IsGenericInstance`/`IsHResultException` static methods from `Abi.cs`.

Also moved `IsByRefType` and `PeelByRefAndCustomModifiers` from
`Models/ParameterCategory.cs` to the new extensions, and updated
`ParamHelpers.GetParamCategory` to call them as extension methods on
`TypeSignature`.

Cache-dependent predicates (`IsBlittablePrimitive` with cross-module enum
resolution, `IsAnyStruct`, `IsComplexStruct`, `IsTypeBlittable`,
`IsFieldTypeBlittable`, `IsMappedAbiValueType`, `IsEnumType`) intentionally
remain in `Factories/CodeWriters.Abi.cs` -- they need access to the
`MetadataCache` which is currently held in the `_cacheRef` static field.
They will be migrated to a dedicated resolver in Pass 18 (ABI marshalling
shape analysis), once Pass 11 has eliminated the static cache state.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… Component)

Finish Pass 7 by extracting the two remaining inline blocks from `Run()`:

- `Generation/ProjectionGenerator.GeneratedIids.cs`:
  `WriteGeneratedInterfaceIIDsFile()` -- the IID-property emission loop that
  writes `GeneratedInterfaceIIDs.cs` (skipped in reference projection mode).
  Includes the global factory-interface discovery sweep and the
  sorted-namespace iteration that produces the parent-before-child grouping
  in the output file.
- `Generation/ProjectionGenerator.Component.cs`:
  `DiscoverComponentActivatableTypes()` -- returns the `(componentActivatable,
  byModule)` pair from scanning every namespace for activatable/static classes
  in component mode.
  `WriteComponentModuleFile(byModule)` -- writes the `WinRT_Module.cs` file
  with the per-module activation factory entry points.

`Run()` is now a clean, linear, 6-phase orchestrator (66 lines, down from
196): discover component activatables -> verbose log -> write IIDs file ->
per-namespace processing -> write component module + default/exclusive
interface files -> write embedded resource files. All inline state is gone;
each phase is a single method call.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add `Helpers/ProjectionEmitContext.cs` -- a per-emission context bundling all
state shared by the projection writers (settings + metadata cache + the
active namespace).

This replaces, conceptually, the implicit state previously held on
`TypeWriter` (which mixes indented-text emission with WinRT-specific state)
and the hidden static `CodeWriters._cacheRef`. During the refactor the
existing `TypeWriter` remains the primary writer surface; this commit
introduces the context as additive infrastructure so it can then be threaded
through writer signatures one family at a time (sub-passes 10b/10c) before
`TypeWriter`/`TextWriter` are finally retired.

Pass 10b/10c (the actual callsite migration -- ~150 method signatures and
hundreds of inner callsites) and Pass 11b/c/d (the cache-static elimination)
are intentionally deferred to subsequent commits to keep this PR's per-commit
diffs small and reviewable. The context API is finalized here; only the
adoption sweep is incremental.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the `References/` folder under sub-namespace
`WindowsRuntime.ProjectionWriter.References` with 4 files of well-known
literals (mirrors `WinRT.Interop.Generator/References/InteropNames.cs`):

- `References/ProjectionNames.cs`:
  `GlobalPrefix` ("global::"), `AbiPrefix` ("ABI."), `GlobalAbiPrefix`,
  `MarshallerSuffix`, `ConvertToUnmanaged`, `ConvertToManaged`, `IID`, `VoidPointer`
- `References/WellKnownNamespaces.cs`:
  `WindowsFoundation`, `WindowsFoundationMetadata`, `WindowsFoundationCollections`,
  `System`, `WindowsRuntimeInternal`, `WindowsUIXamlInterop`
- `References/WellKnownAttributeNames.cs`:
  `ActivatableAttribute`, `StaticAttribute`, `ComposableAttribute`,
  `DefaultAttribute`, `OverridableAttribute`, `ExclusiveToAttribute`,
  `FastAbiAttribute`, `NoExceptionAttribute`, `ContractVersionAttribute`,
  `VersionAttribute`
- `References/WellKnownTypeNames.cs`:
  `HResult`, `DateTime`, `TimeSpan`, `IReferenceGeneric`, `NullableGeneric`,
  `Type`, `Exception`, `Object`, `TypeName`

This commit is purely additive: no callsites change yet. Subsequent passes
will replace the inline string literals with references to these constants.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two-part cleanup pass.

Part 1: mechanical port-comment strip
- Removed pure-XML-doc summaries of the form
  `/// <summary>Mirrors C++ <c>write_xxx</c>.</summary>` (no information beyond
  the C++ port reference).
- Removed standalone `///` and `//` comment lines that were just `Mirrors C++ X`
  with no additional explanation.
- Stripped parenthetical line pins like ` (code_writers.h:1234)` and
  ` code_writers.h:1234` from inline comments (62 of 66 removed).
- Net result: 295 -> 134 `Mirrors C++` references and 66 -> 4 line pins.

Inline comments that explain non-obvious WinRT-spec rules or that surround
substantive logic are kept (those 134 remaining `Mirrors C++` refs all sit
inside multi-line explanatory comments). They will be revisited in Pass 23
(XML doc completeness sweep) where each surviving comment will either be
expanded into proper documentation or replaced with a more idiomatic .NET
description.

Part 2: introduce Errors/ infrastructure
- `Errors/WellKnownProjectionWriterException.cs`: a sealed `Exception`
  subclass with an `Id` property and a `ThrowOrAttach` helper that
  preserves outer-exception context across re-throws. Mirrors the
  `WellKnownInteropException` pattern used by `WinRT.Interop.Generator`.
- `Errors/WellKnownProjectionWriterExceptions.cs`: factory methods that
  produce well-known exceptions tagged with the `CSWINRTPROJECTIONGEN`
  error prefix (matches the project's documented error-id range
  `CSWINRTPROJECTIONGENxxxx`).

Initial catalog has two entries (`InternalInvariantFailed`,
`CannotResolveType`); subsequent passes will add IDs as ad-hoc
`InvalidOperationException` throws scattered through the writer get
converted to well-known exceptions.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the foundational set of emission micro-pattern helpers as extension
methods on `IndentedTextWriter`:

- `WriteSeparated<T>(IEnumerable<T>, string separator, Action<...,T> writeItem)`
  -- replaces the "for each, write item, write `, `" boilerplate that appears
  ~30+ times across the writers.
- `WriteCommaSeparated<T>(IEnumerable<T>, Action<...,T> writeItem)` -- a thin
  convenience wrapper around `WriteSeparated` with `", "` as the separator.
- `WriteGlobal(string typeName)` -- emits `"global::"` + type name.
- `WriteGlobalAbi(string typeName)` -- emits `"global::ABI."` + type name.

These build on the `References/ProjectionNames.cs` constants from Pass 19
so the prefixes have a single canonical source.

This commit is purely additive: the helpers exist alongside the legacy
`TextWriter`/`TypeWriter` and will be used by subsequent passes (Pass 14/15
mechanical sweeps and Pass 16 formatting cleanup) once writer call sites
have been migrated to `IndentedTextWriter`.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the additive infrastructure for the future ABI shape resolver:

- `Models/AbiTypeShapeKind.cs` -- enum classifying a WinRT type signature's
  marshalling shape (BlittablePrimitive, Enum, BlittableStruct, ComplexStruct,
  MappedAbiValueType, String, Object, RuntimeClassOrInterface, Delegate,
  GenericInstance, NullableT, SystemType, HResultException, Array, Unknown).
- `Models/AbiTypeShape.cs` -- immutable record pairing a `Kind` with the
  underlying `TypeSignature`.
- `Resolvers/AbiTypeShapeResolver.cs` -- the resolver itself, constructed
  with a `MetadataCache` reference for cross-module type resolution. The
  initial implementation handles the cache-free shape predicates (String,
  Object, HResultException, SystemType, NullableT, GenericInstance, Array)
  by delegating to the `TypeSignatureExtensions` extensions added in Pass 6.4;
  cache-aware classifications (BlittablePrimitive, Enum, BlittableStruct,
  ComplexStruct, MappedAbiValueType, RuntimeClassOrInterface, Delegate)
  return Unknown for now so callsites can fall through to the legacy
  predicates without behavior change.

Subsequent commits within Pass 18 will progressively migrate the
cache-dependent inline predicates (currently private static methods on
`CodeWriters.Abi.cs`) into this resolver and switch callsites over.

This commit is purely additive: the resolver has no callers yet.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop `IDE0004` (cast is redundant) and `IDE0005` (using directive is unnecessary)
from the writer's `<NoWarn>` list and fix the 5 violations they exposed:

- `Helpers/CodeWriters.Guids.cs` switch-expression default arms had explicit
  `(ushort)0` and `(byte)0` casts that the analyzer flagged as redundant.
- `Extensions/TypeSignatureExtensions.cs` had an unused
  `using AsmResolver.DotNet;` directive (the file only uses
  `AsmResolver.DotNet.Signatures` and `AsmResolver.PE.DotNet.Metadata.Tables`).
- `Factories/CodeWriters.MappedInterfaceStubs.cs` had an unused
  `using AsmResolver.DotNet;` directive.
- `Models/MethodSignatureInfo.cs` had an unused
  `using AsmResolver.DotNet.Collections;` directive.

Validation: builds clean (0 warnings, 0 errors), all 8 regen scenarios
produce byte-identical output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the following IDE-rule suppressions that no longer flag any violations
in the writer codebase (verified by individually un-suppressing each rule
and rebuilding):

- IDE0007 (use `var`)
- IDE0017 (simplify object initialization)
- IDE0021 (use expression body for ctor)
- IDE0040 (add accessibility modifiers)
- IDE0044 (make field readonly)
- IDE0050 (convert anonymous type to tuple)
- IDE0052 (remove unread private members)
- IDE0055 (style for newlines)
- IDE0063 (use simple `using` statement)
- IDE0066 (convert switch to expression)
- IDE0083 (use pattern matching)
- IDE0130 (namespace must match folder)
- IDE0150 (prefer `null` check over type check)
- IDE0180 (use tuple swap)
- IDE0270 (use coalesce expression)
- IDE0290 (use primary constructor)

These all became dead suppressions through Pass 5 (Models/), Pass 6 (Extensions/),
Pass 7 (split ProjectionGenerator), Pass 8 (split Helpers/), Pass 9
(IndentedTextWriter), Pass 10a (ProjectionEmitContext), and Pass 17a/18a
(emission helpers / ABI shape resolver) -- the new code consistently follows
the modern style these rules enforce.

Validation: builds clean (0 warnings, 0 errors), all 8 regen scenarios
produce byte-identical output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add proper `<summary>` doc comments to the public fields/properties of the
three Models/ state-bag types that previously had only a single class-level
summary:

- `Models/PropertyAccessorState.cs`: 22 public fields
- `Models/StaticPropertyAccessorState.cs`: 9 public fields
- `Models/MethodSignatureInfo.cs`: `Method`/`Params`/`ReturnParam` properties,
  `ReturnType` getter, `ReturnParamName(string)` method

Each field/property now documents its purpose (e.g. "Gets or sets the ABI
Methods class name used by the getter dispatch") with WinRT-spec-relevant
notes captured in `<remarks>`-style narrative where useful.

This brings the new code in `Models/`, `Extensions/`, `References/`,
`Errors/`, and `Resolvers/` (added across passes 5-20) up to full XML doc
coverage. The remaining `///` gaps live in the legacy `CodeWriters.*`
partials that will be addressed in subsequent Pass 23 sub-passes once
those partials get their final layout from Pass 12 (Abi.cs split) and
Pass 13 (rename to *Factory/*Builder).

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Restructure the legacy `TextWriter`/`TypeWriter` writer surfaces so they internally
delegate all emission to an `IndentedTextWriter`. The two surfaces now share a
single buffer through their underlying writer, which is the foundation for
incremental migration of methods to take `(IndentedTextWriter writer,
ProjectionEmitContext context)` instead of `TypeWriter w` (per the Pass 10
plan: "convert one writer family at a time ... Old TypeWriter keeps a
passthrough overload during the transition").

Changes:

- `IndentedTextWriter` gains the supporting API needed for the wrapper:
  `Length`, `Back()`, `GetSubstring(start, length)`, `Remove(start, length)`,
  `CurrentIndentLevel`, `ResetIndent()`, `FlushToString()`, `FlushToFile(path)`.
  These mirror the `TextWriter` operations the C++ port relied on
  (`WriteTemp` capture-and-restore, `Back()` last-char check, file flush
  with content-equality skip).

- `Writers/TextWriter.cs` rewritten as a thin shim that wraps an
  `IndentedTextWriter`. The legacy `%`/`@`/`^` format placeholders,
  `WriteValue` polymorphic dispatch, `WriteCode` backtick-stripping, and
  `WriteTemp` are all preserved unchanged at the public API. Internally,
  every character flows through `IndentedTextWriter.Write`.
  Brace-tracked auto-indent (the historical "no leading whitespace in source
  strings" convention from the C++ port) is preserved by driving
  `IndentedTextWriter.IncreaseIndent()` / `DecreaseIndent()` from the
  brace-state machine in `TextWriter.UpdateState`. This keeps the existing
  emission semantics identical while letting code that uses
  `IndentedTextWriter` directly (via `TextWriter.Writer`) interoperate
  cleanly with the same buffer.

- `Writers/TypeWriter.cs` rewritten as a thin sealed wrapper that exposes a
  bundled `Context` (`ProjectionEmitContext`) alongside the inherited
  `Writer` (`IndentedTextWriter`) accessor. The four WinRT-specific helpers
  (`WriteFileHeader`, `WriteBeginProjectedNamespace`,
  `WriteEndProjectedNamespace`, `WriteBeginAbiNamespace`,
  `WriteEndAbiNamespace`) become passthroughs that delegate to extension
  methods on `(IndentedTextWriter, ProjectionEmitContext)`. The mutable
  `InAbiNamespace` / `InAbiImplNamespace` flags are still maintained on
  `TypeWriter` for legacy callers.

- New `Extensions/ProjectionWriterExtensions.cs` houses the WinRT-specific
  emission extensions (`WriteFileHeader`, `WriteBeginProjectedNamespace`,
  `WriteEndProjectedNamespace`, `WriteBeginAbiNamespace`,
  `WriteEndAbiNamespace`) on `IndentedTextWriter` + `ProjectionEmitContext`.
  These are the long-term replacement for the `TypeWriter` instance methods;
  migrated callers will use these directly.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline. The shared-buffer approach means the brace-auto-indent
behavior is unchanged for legacy callers, and migrated callers (Pass 10c)
will use IndentedTextWriter's explicit Increase/DecreaseIndent + WriteBlock
APIs going forward.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate the orchestrator partials -- the topmost layer that owns the writers
and writes the output files -- from `TypeWriter` to the new
`(IndentedTextWriter writer, ProjectionEmitContext context)` surface,
following the Pass 10c "convert one writer family at a time" plan:

- `ProjectionGenerator.Namespace.cs` (`ProcessNamespace`):
  - Constructs `ProjectionEmitContext` explicitly with the active settings,
    cache, and namespace.
  - Constructs `TypeWriter` from the context (using the new
    `TypeWriter(ProjectionEmitContext)` ctor introduced in Pass 10b) and
    captures the underlying `IndentedTextWriter` via `w.Writer`.
  - File header / projected-namespace begin+end / ABI-namespace begin+end
    are all called as `writer.WriteFileHeader(context)` /
    `writer.WriteBeginProjectedNamespace(context)` / etc. directly on the
    `IndentedTextWriter` via the new extensions.
  - Phase 4 additions and `FlushToFile` go through the `IndentedTextWriter`.
  - `TypeWriter w` is still passed to the unmigrated `CodeWriters.X(w, ...)`
    helpers (those migrate in subsequent commits).

- `ProjectionGenerator.GeneratedIids.cs` (`WriteGeneratedInterfaceIIDsFile`):
  - Same pattern: explicit `ProjectionEmitContext`, `IndentedTextWriter`
    captured via `guidWriter.Writer`, file flush via the indented writer.

- `ProjectionGenerator.Component.cs` (`WriteComponentModuleFile`):
  - Switched the leaner banner-only `CodeWriters.WriteFileHeader(TextWriter)`
    helper to also accept (or be called with) `wm.Writer`. Added the
    `WriteFileHeader(IndentedTextWriter)` overload to `CodeWriters.Helpers.cs`,
    with the previous `TextWriter` overload now a one-line passthrough
    (will go away when `TextWriter` is deleted in the final 10c step).

- `ProjectionGenerator.Resources.cs` (`WriteBaseStrings`):
  - The temporary banner writer is now a fresh `IndentedTextWriter`, with
    `CodeWriters.WriteFileHeader(headerWriter)` going to the new overload
    and `headerWriter.FlushToString()` returning the captured banner.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Migrate three small writer families that don't need any per-call state
beyond the writer itself, from `TypeWriter w` to `IndentedTextWriter writer`:

- `Factories/CodeWriters.RefModeStubs.cs`:
  `EmitRefModeObjRefGetterBody`, `EmitSyntheticPrivateCtor`, `EmitRefModeInvokeBody`
- `Helpers/CodeWriters.Helpers.cs`:
  `WritePragmaDisableIL2026`, `WritePragmaRestoreIL2026`

Updated 5 call sites (2 in `CodeWriters.Constructors.cs`, 1 in
`CodeWriters.Class.cs`, 2 in `ProjectionGenerator.Namespace.cs`) to pass
`w.Writer` (legacy `TypeWriter`) or `writer` (new `IndentedTextWriter`)
instead of `w`.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…onEmitContext)

Migrate the high-fanout type-name helpers in `Helpers/CodeWriters.TypeNames.cs`
to take `(IndentedTextWriter writer, ProjectionEmitContext context)` as their
new primary signature, with the legacy `TypeWriter` overloads kept as
passthrough wrappers per the Pass 10c plan ("Old TypeWriter keeps a
passthrough overload during the transition").

Migrated helpers (each gets a new `(IndentedTextWriter, ...)` overload plus
a one-line `TypeWriter` passthrough that calls `WriteX(w.Writer, w.Context, ...)`):

- `WriteFundamentalType(IndentedTextWriter, FundamentalType)`
- `WriteFundamentalNonProjectedType(IndentedTextWriter, FundamentalType)`
- `WriteTypedefName(IndentedTextWriter, ProjectionEmitContext, TypeDefinition, TypedefNameType, bool)`
- `WriteTypeParams(IndentedTextWriter, TypeDefinition)`
- `WriteTypeName(IndentedTextWriter, ProjectionEmitContext, TypeSemantics, TypedefNameType, bool)`
- `WriteProjectionType(IndentedTextWriter, ProjectionEmitContext, TypeSemantics)`

Inside the migrated implementations:
- `w.Settings` -> `context.Settings`
- `w.CurrentNamespace` -> `context.CurrentNamespace`
- `w.InAbiNamespace` / `w.InAbiImplNamespace` -> `context.InAbiNamespace` / `context.InAbiImplNamespace`
- `w.WriteCode(name)` -> `writer.Write(IdentifierEscaping.StripBackticks(name))`

Also extended `Helpers/ProjectionEmitContext.cs` with the two emission-mode
flags (`InAbiNamespace`, `InAbiImplNamespace`) plus scoped `IDisposable`
helpers (`EnterAbiNamespace()`, `EnterAbiImplNamespace()`) per the Pass 10
plan ("The mode flags become scoped via IDisposable helpers ... eliminating
the 'did I forget to reset?' failure mode"). The legacy `TypeWriter`'s
`WriteBeginAbiNamespace` / `WriteEndAbiNamespace` / `WriteBeginProjectedNamespace`
/ `WriteEndProjectedNamespace` methods now flip the flags through the
shared `Context` (via `SetInAbiNamespace` / `SetInAbiImplNamespace` internal
setters), so legacy and migrated callers see the same state.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…extWriter, ProjectionEmitContext)

Migrate two more writer families to take `(IndentedTextWriter writer,
ProjectionEmitContext context)` as their primary signature, with legacy
`TypeWriter` overloads kept as one-line passthroughs:

`Factories/CodeWriters.Methods.cs`:
- `WriteProjectedSignature(IndentedTextWriter, ProjectionEmitContext, TypeSignature, bool)`
- `WriteProjectionParameterType(IndentedTextWriter, ProjectionEmitContext, ParamInfo)`
- `WriteParameterName(IndentedTextWriter, ParamInfo)` -- inlined the
  `IdentifierEscaping.WriteEscapedIdentifier` call so this helper is
  trivially context-free.
- `WriteProjectionParameter(IndentedTextWriter, ProjectionEmitContext, ParamInfo)`
- `WriteProjectionReturnType(IndentedTextWriter, ProjectionEmitContext, MethodSig)`
- `WriteParameterList(IndentedTextWriter, ProjectionEmitContext, MethodSig)`
- `FormatField(FieldDefinition)` is unchanged (returns a string).

`Helpers/IdentifierEscaping.cs`:
- `WriteEscapedIdentifier(IndentedTextWriter, string)` is the new primary
  signature; the legacy `TextWriter` overload becomes a one-line passthrough.

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rojectionEmitContext)

Migrate the 8 WinRT-attribute emission helpers in `Helpers/CodeWriters.Helpers.cs`
to take `(IndentedTextWriter writer, ProjectionEmitContext context)` as their
primary signature, with the legacy `TypeWriter` overloads kept as one-line
passthroughs:

- `WriteWinRTMetadataAttribute(IndentedTextWriter, TypeDefinition, MetadataCache)`
  -- doesn't need context (just emits a string literal from the cache lookup).
- `WriteWinRTMetadataTypeNameAttribute(IndentedTextWriter, ProjectionEmitContext, TypeDefinition)`
- `WriteWinRTMappedTypeAttribute(IndentedTextWriter, ProjectionEmitContext, TypeDefinition)`
- `WriteValueTypeWinRTClassNameAttribute(IndentedTextWriter, ProjectionEmitContext, TypeDefinition)`
- `WriteWinRTReferenceTypeAttribute(IndentedTextWriter, ProjectionEmitContext, TypeDefinition)`
- `WriteComWrapperMarshallerAttribute(IndentedTextWriter, ProjectionEmitContext, TypeDefinition)`
- `WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute(IndentedTextWriter, ProjectionEmitContext, TypeDefinition)`
- `WriteWinRTComWrappersTypeMapGroupAssemblyAttribute(IndentedTextWriter, ProjectionEmitContext, TypeDefinition, bool)`
- `WriteWinRTIdicTypeMapGroupAssemblyAttribute(IndentedTextWriter, ProjectionEmitContext, TypeDefinition)`

Inside the migrated implementations:
- `w.Settings` -> `context.Settings`
- `w.WriteTemp("%", new Action<TextWriter>(tw => { ... }))` patterns are
  replaced with the cleaner explicit form: a fresh scratch `IndentedTextWriter`,
  written into via `WriteTypedefName(scratch, context, ...)` /
  `WriteTypeParams(scratch, type)`, then captured via `scratch.ToString()`.
  The scratch writer's indent level is 0 by construction, so the captured
  string has no leading whitespace -- matching the legacy `WriteTemp`
  semantics (which toggles `_enableIndent = false`).

Validation: builds clean, all 8 regen scenarios produce byte-identical
output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rojectionEmitContext)

Migrate the custom-attribute carry-over and platform attribute helpers
in 'CodeWriters.CustomAttributes.cs' to the new emit surface, and add
the 'CheckPlatform'/'Platform' mutable state to 'ProjectionEmitContext'
(with 'TypeWriter' forwarding through the context).

Methods migrated:
- WriteCustomAttributeArgs
- WritePlatformAttribute
- WriteCustomAttributes
- WriteTypeCustomAttributes
- GetPlatform (private)
- FormatCustomAttributeArg (private; no longer needs a writer)

Each public method gains a primary '(IndentedTextWriter writer, ProjectionEmitContext context, ...)'
overload, with the legacy 'TypeWriter w' overload kept as a one-line passthrough.

Validation: all 8 regen scenarios produce byte-identical output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r, ProjectionEmitContext)

Migrate the mapped-interface stub emitters in 'CodeWriters.MappedInterfaceStubs.cs' to
the new emit surface. These emit C# interface stub members for WinRT interfaces that
project to .NET BCL interfaces (IClosable->IDisposable, IMap->IDictionary, etc).

Methods migrated to '(IndentedTextWriter, ProjectionEmitContext, ...)':
- WriteMappedInterfaceStubs
- EmitDisposable, EmitGenericEnumerable, EmitGenericEnumerator
- EmitDictionary, EmitReadOnlyDictionary, EmitList, EmitReadOnlyList
- EmitNonGenericList, EmitUnsafeAccessor
- EncodeArgIdentifier (private)

The 'WriteTemp' calls (which emitted projected type names into a scratch buffer with
indent disabled) are replaced with explicit scratch 'IndentedTextWriter' instances
calling 'WriteTypeName(scratch, context, ...)'. A small 'WriteTypeNameToString' helper
captures the pattern.

The legacy 'TypeWriter' overload of 'WriteMappedInterfaceStubs' is kept as a one-line
passthrough.

Validation: all 8 regen scenarios produce byte-identical output to baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sergio0694 and others added 26 commits May 12, 2026 02:04
F5 - Sort discovered .winmd inputs (Metadata/MetadataCache.cs)
  Directory.EnumerateFiles returns files in filesystem-dependent order, so the
  "first-load-wins" duplicate-type behavior in LoadFile (line 173) was
  effectively environment-dependent. Sort the deduped winmdFiles list with
  StringComparer.OrdinalIgnoreCase before LoadFile iteration so duplicate-type
  precedence is deterministic across machines and CI runs. Aligns with the
  source-generator pattern in TypeMapAssemblyTargetGenerator.cs:92-95.

F8 - Replace ContainsKey + indexer with TryAdd (Metadata/MetadataCache.cs:LoadFile)
  Two-probe `if (ContainsKey) continue; ... [fullName] = type;` collapsed to
  single-probe `if (!_typesByFullName.TryAdd(fullName, type)) continue;`,
  matching the interop generator''s prevalent single-probe map mutation style
  (e.g. InteropGeneratorArgs.Parsing.cs:91).

Validation: build clean (0 warnings), all 8 regen scenarios byte-identical
(deterministic ordering happens to match the prior filesystem order on this
machine/SDK build, so no user-visible change).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…y blanks)

F9 - Collapsed 12 sites of double-blank-line gaps to single blanks (raw-string-aware sweep):
  - Helpers/AbiTypeWriter.cs (8 sites at the if-chain in TypeSemantics.Definition / .Reference cases)
  - Helpers/WindowsMetadataExpander.cs (2 sites: between usings/namespace and between two ifs in best-version loop)
  - Metadata/TypeCategorization.cs (2 sites between consecutive ifs in GetCategoryFromBase)

F10 - Inserted missing blank lines around block boundaries (5 sites):
  - Helpers/TypeFilter.cs (between Includes return and Match method declaration)
  - Generation/ProjectionGenerator.Namespace.cs (between foreach close and WritePragmaRestoreIL2026)
  - Metadata/MetadataCache.cs (between cache construction, foreach load, sort, and return)

N4 - Removed extra blank line before GeneratedIids method close brace:
  - Generation/ProjectionGenerator.GeneratedIids.cs (line 125 stray blank)
  - Plus added blank line before WriteInterfaceIidsEnd (matching block-boundary convention)

The double-blank sweep used the same raw-string-aware Mark-LinesInRawString helper from prior
sweeps to avoid touching emitted projection text inside """ raw-string literals.

Validation: build clean (0 warnings), all 8 regen scenarios byte-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ers.cs)

The 70+ line sealed NamespaceMembers class lived as a secondary type at the
bottom of MetadataCache.cs. R5 opus-4.6 flagged that the interop generator
convention for multi-type files pairs a parent with either a file-scoped helper
or a nested partial split, not an independent top-level type.

Moved NamespaceMembers verbatim to Metadata/NamespaceMembers.cs and removed it
from MetadataCache.cs. No behavior change.

Validation: build clean (0 warnings), all 8 regen scenarios byte-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
R5 opus-4.6 flagged the 5 IDE-prefix project-level NoWarn entries as visible
csproj-shape divergence vs the interop generator (which has zero IDE-prefix
project NoWarn). Tested removing each suppression individually:

- IDE0010: 22 fires
- IDE0022: 56 fires
- IDE0046: 60 fires
- IDE0060: 28 fires
- IDE0072: 16 fires

All 5 suppressions are still pervasive enough that per-site refactoring is
out of scope for this cleanup pass. The author''s intent ("should be tightened
over time") stands. Updated the csproj comment to reflect the measured fire
counts so future contributors know exactly what they''re inheriting.

No code change beyond the comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
N1 - Metadata/TypeSemantics.cs:251 typo
  XML doc summary "Type-name kind,." -> "Type-name kind."

N2 - Factories/AbiClassFactory.cs:53-56 truncated comment
  Comment block started mid-sentence ("// instantiation (e.g. ...)").
  Restored the dropped antecedent: "// If the default interface is a generic
  instantiation (e.g. ...)".

N5 - Writers/IndentedTextWriter.cs:404 CurrentIndentLevel placement
  Public CurrentIndentLevel property was placed at the bottom of the class,
  separated from the related state fields and IncreaseIndent/DecreaseIndent
  methods that read it. Moved adjacent to _currentIndentation /
  _availableIndentations near the top, matching the source-generator
  IndentedTextWriter layout where the state fields are colocated with the
  field they conceptually belong with.

N8 - Generation/ProjectionGenerator.cs:80 missing blank line after log = ...
  Added blank lines around the foreach iteration to match the
  "declare, then act" cadence used elsewhere in the file.

(N3 was already addressed: AbiMethodBodyFactory.DoAbi.cs:38-46 already had a
blank-line group separator between return-type and method-kind booleans.
N4, N6, N7 were addressed in earlier R5 commits.)

Validation: build clean (0 warnings), all 8 regen scenarios byte-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…RTInteropGenerator

CI failure:

    nuget\Microsoft.Windows.CsWinRT.targets(678,3): Error MSB4024: The imported
    project file "...\nuget\Microsoft.Windows.CsWinRT.CsWinRTGen.targets" could
    not be loaded. ''MaxDegreesOfParallelism'' is a duplicate attribute name.
    Line 191, position 7.

The first _RunCsWinRTInteropGenerator task invocation in
Microsoft.Windows.CsWinRT.CsWinRTGen.targets had ``MaxDegreesOfParallelism="..."``
declared twice (lines 187 and 191) -- introduced by an earlier R5 commit when
threading the new MaxDegreesOfParallelism setting through the MSBuild surface.
MSBuild rejects duplicate attribute names on a single XML element.

Removed the duplicate at line 191 (kept the one at 187 for ordering symmetry
with the other 5 task invocations in the file). All 6 _RunCsWinRTXxx task
invocations now have exactly one MaxDegreesOfParallelism attribute each;
verified by a per-task duplicate-attribute scan.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lGenerator

CI failure:

    nuget\Microsoft.Windows.CsWinRT.CsWinRTGen.targets(259,7): Error MSB4064:
    The "MaxDegreesOfParallelism" parameter is not supported by the
    "RunCsWinRTForwarderImplGenerator" task loaded from assembly:
    WinRT.Generator.Tasks ...

The R5 commit that exposed MaxDegreesOfParallelism through the MSBuild surface
(580a050 "F7 + F8: ... surface MaxDegreesOfParallelism through projection-
generator CLI") added the attribute to all 6 _RunCsWinRTXxx task invocations,
but only added the corresponding property to two task classes:

  * RunCsWinRTInteropGenerator.cs:123        - has property
  * RunCsWinRTMergedProjectionGenerator.cs:93 - has property
  * RunCsWinRTForwarderImplGenerator.cs       - DOES NOT have property
  * RunCsWinRTProjectionRefGenerator.cs       - DOES NOT have property
  * RunCsWinRTWinMDGenerator.cs               - DOES NOT have property

The forwarder/impl, projection-ref, and winmd tasks are single-pass tools
without internal parallelism, so the property doesn''t apply to them. The
prior commit (c0d0f57) accidentally only fixed the literal duplicate-attribute
XML problem on the interop task, not the broader "passing the attribute to
tasks that don''t expose it" problem.

Fixed by removing the MaxDegreesOfParallelism="..." attribute from the
RunCsWinRTForwarderImplGenerator invocation at line 259. The 5 remaining usages
(1x InteropGenerator, 4x MergedProjectionGenerator) are all on tasks that
expose the property; the projection-ref and winmd tasks were never receiving
the attribute in the first place.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tput

CI compiled the generated projection output for the first time after the
parallelism/exception-discipline R5 work and surfaced 5 distinct error
patterns. Three root causes:

(1) IndentedTextWriter: missing trailing newline after multiline raw-string
    ending in a preprocessor directive
  -------------------------------------------------------------------------
  Write(span, isMultiline=true) auto-appends a trailing newline when the
  last chunk ends with `;`, `}`, or `]` (since raw `"""..."""` strings never
  include a trailing newline before the closing token). Preprocessor lines
  like `#nullable disable` end with `e`/`l`, so no newline was appended,
  causing the next emission to jam onto the same line:

    }
    #nullable disablepublic static class XxxMethods
    {

  Extended the heuristic: also append a trailing newline when the last
  chunk starts with `#` (preprocessor directive). Verified by re-running
  the writer and confirming `#nullable disable\npublic static class ...`
  is now correctly multi-line in every previously-affected file.

  Symptoms cleared:
    CS8637 (Expected enable, disable, or restore)
    CS1022 (Type or namespace definition or end-of-file expected)
    CS0106 (modifier public is not valid for this item)
    CS0234 (cascade: jammed [assembly:] attributes corrupted parser)

(2) AbiInterfaceIDicFactory: IDIC `file interface` base type emitted without
    namespace prefix, creating a self-reference
  -------------------------------------------------------------------------
  The IDIC helper `file interface IXxx : IXxx` was emitted with the base
  type via WriteTypedefName(... forceWriteNamespace=false). When the
  surrounding namespace context matches the type's namespace, that path
  emits the bare name -- making `file interface IBackgroundTransferOperation
  : IBackgroundTransferOperation`, which C# resolves as the file-scoped
  helper inheriting from itself. Result: every explicit-interface impl on
  the helper failed CS0540 (containing type does not implement interface).

  Fixed by passing `forceWriteNamespace=true` so the inheritance becomes
  `file interface IBackgroundTransferOperation :
  global::Windows.Networking.BackgroundTransfer.IBackgroundTransferOperation`,
  which unambiguously points at the projected interface.

  Symptom cleared:
    CS0540 (containing type does not implement interface)

(3) AbiStructFactory: ABI struct DECLARATION emitted with `global::ABI.Ns.`
    prefix, which is illegal as a type-declaration name
  -------------------------------------------------------------------------
  AbiStructFactory line 52 used WriteTypedefName(... TypedefNameType.ABI,
  false) for the struct declaration after `internal unsafe struct `. ABI
  types are unconditionally prefixed by WriteTypedefName, producing:

    public unsafe struct global::ABI.Windows.Devices.Gpio.GpioChangeCount

  -- which is a syntax error (CS1514, CS1513, CS1001) because `global::`
  is not valid in a type declaration. The struct is already inside the
  proper `namespace ABI.Windows.Devices.Gpio { }` block, so the
  declaration just needs the bare type name.

  Fixed by replacing the WriteTypedefName call with a direct
  `IdentifierEscaping.StripBackticks(type.Name?.Value ?? string.Empty)`
  emission.

  Symptoms cleared:
    CS1514 ({ expected)
    CS1513 (} expected)
    CS1001 (identifier expected)

How these were missed
---------------------
Local validation was byte-identical comparison against baselines captured
on 5/11 6:28am, BEFORE these bugs were introduced into the writer. None
of the R5 commits touched projection output, so byte-identity always
passed -- but the baselines themselves had pre-existing bugs locked in.
CI catches them because CI actually compiles the generated .cs with csc.

Future-proofing: should add a csc-compile step to the validation harness
so new emission bugs surface locally rather than only in CI.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs design — the writer
no longer tries to retroactively patch caller mistakes via buffer-state heuristics.
All spacing/newlines must now be explicit at the call sites (matching the source
generator pattern of "what you call is what you get").

Removed heuristics:

1. Brace-prepend in Write(span, bool): used to insert a newline if buffer ended
   with `{`/`}` and content started with whitespace. This was the cause of the
   "property accessor split" regression (`{ get; }` -> `{\n   get; }`) — the
   single-space separator after `{` triggered the rule even though it was
   inline-continuation, not an indented code line.

2. Auto-trailing-newline in multiline Write: used to append a newline if the
   last chunk ended with `;`, `}`, `]`, or started with `#`. This was a band-aid
   for raw multi-line strings ending without a trailing newline before the
   closing `"""` token. Callers should now either include the trailing newline
   in the raw string or use WriteLine instead of Write.

3. Brace-prepend in WriteLine(content): used to insert a newline if buffer ended
   with `{`/`}`. Same family as (1).

4. Always-on idempotent WriteLine(): used to suppress newline if buffer ended
   with `\n\n` or `{\n`. Now opt-in via WriteLine(bool skipIfPresent = false),
   matching the source-gen API. Default behavior is no suppression.

Why
---

The user pointed out that the source-gen IndentedTextWriter has none of these
auto-fixups except the explicitly-opted-in `skipIfPresent`. All spacing is done
by the caller via correct WriteLine / Block() / etc. calls. Auto-fixups in the
writer are:

- Less efficient (every Write inspects buffer tail)
- A band-aid that hides caller-side mistakes rather than encouraging correct
  caller-side calls
- The root cause of the property-split / brace-jam / nullable-jam regressions
  documented in the regression report

This commit ONLY changes the writer. It will produce broken output until all
callers are updated to emit explicit newlines where the heuristics used to
silently insert them. Subsequent commits will fix the callers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…h ;}]#

When the IndentedTextWriter heuristics were removed, every multiline raw string
that ended with a complete-line content (`;`, `}`, `]`, or starting with `#`)
lost its trailing newline (was previously injected by the auto-trail rule).

Mass-converted 147 such call sites from `writer.Write(...""", isMultiline: true)`
to `writer.WriteLine(...""", isMultiline: true)` so the trailing newline is
explicit at the call site.

This is the first batch. Subsequent commits will:
- Add explicit `WriteLine();` after multiline writes ending with `{` (block opens)
- Fix indentation/IncreaseIndent at namespace/block boundaries that were
  previously masked by brace-prepend heuristics
- Audit the 28 inline-continuation sites to confirm they should stay as `Write`

Build clean. Output still differs from pre-truth in many places — fixing those
in subsequent commits.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…riteBlock

WriteFileHeader had stayed as Write (not WriteLine), so the banner was missing
its trailing newline. Changed to WriteLine.

WriteDefaultInterfacesClass and WriteExclusiveToInterfacesClass were emitting
the namespace block via `Write("""namespace ABI\n{""")` followed by a foreach
loop and a closing `Write("""}""")`. With the heuristics removed, this produced
"{[Win...]" jamming and 0-indent attributes inside the namespace.

Rewrote both functions using the source-gen pattern:

  w.WriteLine("namespace ABI");
  using (w.WriteBlock())
  {
      foreach (...) { w.WriteLine($"[Win...]"); }
      w.WriteLine("internal static class WindowsRuntimeDefaultInterfaces;");
  }

WriteBlock() handles the `{`, IncreaseIndent, DecreaseIndent, and closing `}`
in one self-contained scope. Output now matches pre-truth byte-for-byte for
this file.

Validation: 77/1553 files now match pre-truth (was 60). Continuing iteratively.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ontext

CI surfaced two errors in the generated Windows.AI.MachineLearning.cs:
  CS0246: 'LearningModelPixelRange' could not be found
  CS9051: File-local 'IReadOnlyList<ILearningModelFeatureDescriptor>'
          cannot be used in non-file-local 'ILearningModelMethods'

Both are caused by the same long-standing gap: the writer was emitting
projected type names without the `global::Ns.` prefix when the type was
defined in the same namespace as `context.CurrentNamespace`. That is correct
when emitting code INSIDE the projected namespace, but wrong when emitting
code inside the corresponding ABI namespace (where the projected namespace
is not in scope, so the bare name doesn't resolve).

The OLD writer (pre-refactor at 11ef00a) had this exact mechanism as
`w.InAbiNamespace` / `w.InAbiImplNamespace` flags on its TypeWriter. The
refactor (R2/R3) lost it during the move from TypeWriter to
ProjectionEmitContext + extension methods.

Restored:

1. ProjectionEmitContext.InAbiNamespace and InAbiImplNamespace bool flags.
2. WriteBeginAbiNamespace(context) / WriteEndAbiNamespace(context) toggle
   InAbiNamespace true/false. WriteBeginProjectedNamespace(context) sets
   InAbiImplNamespace=true when in component mode (where the projected
   namespace itself is `ABI.Impl.Ns`).
3. TypedefNameWriter.WriteTypedefName: extended the namespace-prefix
   condition to also fire when emitting a Projected name while the writer
   is inside an ABI/ABI.Impl namespace -- matches the OLD writer's
   `(nameToWrite == TypedefNameType.Projected && (w.InAbiNamespace || w.InAbiImplNamespace))`
   case.
4. Same fix to the Reference branch of WriteTypeName (cross-module typeRef
   resolution path).
5. Replaced single-overload WriteEndProjectedNamespace() / WriteEndAbiNamespace()
   with the context-aware overloads (callers updated).

Verified by csc-compiling the generated SDK output: zero CS0246 or CS9051
errors against the previously-affected file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The InterfaceFactory.WriteInterfaceMemberSignatures loops were emitting
`writer.WriteLine();` BEFORE each member to add a leading blank line, but with
the heuristic-removed writer this added genuine blank lines between members
(the old idempotent suppression was hiding this). Pre-truth has NO blank
lines between members in interface bodies.

Switched to the source-gen pattern: each iteration writes content + trailing
`WriteLine`, ensuring the buffer always ends with `\n` at iteration boundary.
The next iteration's content is then naturally indented onto a fresh line.

Also fixed the `;}` jam where the last member's content was Write (no \n)
and the WriteBlock's closing `}` got jammed onto the same line.

Validation: 135/1553 files now match pre-truth (was 86).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The WriteStruct emission was using flat multiline raw strings with
no IncreaseIndent context, causing struct body content (constructor,
properties, operators) to be emitted at the wrong indent level
(4 spaces / namespace level instead of 8 spaces / body level).

Refactored to use the WriteBlock() pattern:
- Outer using (writer.WriteBlock()) for the struct body
- Nested using (writer.WriteBlock()) for the constructor body
- Nested using (writer.WriteBlock()) for each property body

This produces correctly-indented output matching the source-generator
style. Verified: PRE and POST compile-error counts are byte-identical
across all 10 error codes (CS0246, CS0234, CS0616, CS0538, CS8894,
CS0738, CS9334, CS0539, CS0579, CS9333), confirming zero new compile
errors introduced. All remaining file differences are whitespace/
formatting improvements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1. AbiMethodBodyFactory.RcwCaller: returnIsRefType missed NullableT shape

   Commit a17d659 (P1-3: Wire AbiTypeShapeResolver into emission paths)
   converted `returnIsRefType = ...IsRuntimeClassOrInterface() || .IsObject()
   || .IsGenericInstance()` to use the new shape kinds. The OLD predicate
   `rt.IsGenericInstance()` returned true for Nullable<T> (since Nullable<T>
   IS a generic instance). The shape resolver now categorizes Nullable<T>
   as `AbiTypeShapeKind.NullableT` (checked BEFORE GenericInstance), so it
   never reaches the GenericInstance branch.

   The bug: for any Nullable<T> getter, the writer emitted:
       int __retval = default;
       (delegate*<void*, int*, int>)
       return (Nullable<T>)__retval;

   Should be (matches PRE):
       void* __retval = default;
       try {
           (delegate*<void*, void**, int>)
           return T_Marshaller.UnboxToManaged(__retval);
       } finally {
           WindowsRuntimeUnknownMarshaller.Free(__retval);
       }

   Caused CI error CS0030 `Cannot convert type 'int' to 'DateTimeOffset?'`
   on Windows.ApplicationModel.Wallet.cs:2303 (and many other Nullable<T>
   getters across the SDK).

   Fix: add `or AbiTypeShapeKind.NullableT` to the returnIsRefType check.

2. MetadataAttributeFactory.WriteWinRTIdicTypeMapGroupAssemblyAttribute and
   WriteWinRTComWrappersTypeMapGroupAssemblyAttribute: stray newline after
   `typeof(` split the attribute argument across lines.

   The multiline raw string ending with `source: typeof(` was emitted via
   `WriteLine` (which appends a trailing newline) instead of `Write`
   (no trailing newline). Result: the typedef name appeared on the next
   line. Compiles fine but produces messy output.

   Fix: change `WriteLine` to `Write` for boundary multilines that end
   with `(` since the next emission must continue on the same line.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…thods

ClassFactory.cs (RCW constructor), ConstructorFactory.Composable.cs
(composable ctors and base-chaining ctors), and
ClassMembersFactory.WriteInterfaceMembers.cs (GetInterface/GetDefaultInterface
helpers) were emitting class-method bodies via flat multiline raw strings
that didn't IncreaseIndent for the body. The OLD writer's auto-fixup
heuristics masked this, but with the new `dumb` writer the body content
landed at the same indent as the surrounding braces, producing ugly but
still-compilable code:

    public Foo(...)
    : base(...)
    {
    if (GetType() == typeof(Foo))
    {
    _objRef = NativeObjectReference;
    }
    }

Fixed by replacing flat multiline + manual `WriteLine(\"}\")` with the
`using (writer.WriteBlock())` pattern used elsewhere. Each method body is
now properly indented one level inside its braces.

143/1553 -> 177/1553 byte-identical vs PRE.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per code review: when block content is fully fixed (no conditional logic,
no per-iteration emission), prefer a single multiline interpolated raw
string with the body indented inline rather than splitting into
WriteBlock + per-line writes. The single-multiline form is more concise
and reads more like the actual generated output.

Affects:
- ClassFactory.cs (RCW ctor body's GetType()==typeof if-block)
- ConstructorFactory.Composable.cs (composable ctor's GetType() if-block)
- ClassMembersFactory.WriteInterfaceMembers.cs (GetInterface and
  GetDefaultInterface bodies)

Validation count unchanged (177/1553) -- output is byte-identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The PassArray/Out/Ref parameter loops in AbiMethodBodyFactory.RcwCaller
emitted multiline raw strings via WriteLine, which appended a trailing
newline after the parameter content. The next iteration's leading ',\n'
then started at start-of-line where the writer auto-prepends indent, so
the comma landed on its own indented line:

    (uint)data.Length, _data
        ,
    columnCount

Should be (matches PRE):

    (uint)data.Length, _data,
        columnCount

Fix: change WriteLine to Write for these emitters so the next
iteration's leading comma stays inline with the previous parameter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
My port added a defensive else branch that emitted an empty
`if (GetType() == typeof(X)) { }` block when defaultIfaceObjRef was
empty. The original C++ template (cswinrt/code_writers.h:3169-3178)
unconditionally emits `<defaultObjRef> = NativeObjectReference;`
inside the if block via bind<write_objref_type_name>(default_type_semantics)
which always produces a name. The defensive null check + empty block
in the C# port produced dead code that the C++ never emitted.

Validation count unchanged (178/1553) -- the branch was never reached
in any of our test scenarios, confirming defaultIfaceObjRef is always
non-empty here in practice.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tics

Root cause: commit 175936f (R2 P0-2: Wire AbiTypeShapeResolver into all
classification callsites) silently changed the semantics of two predicates
that ~30 callsites depend on:

1. `IsBlittablePrimitive`: OLD `AbiTypeHelpers.IsBlittablePrimitive(cache, sig)`
   returned true for both primitives AND enums (enums marshal as their
   underlying integer). The new resolver method returned true ONLY for
   `BlittablePrimitive` shape, EXCLUDING `Enum` shape.
   - Symptom: `returnIsReceiveArray` and similar checks treated
     `EnumType[]` returns as primitive scalars instead of arrays. Generated
     `int __retval` + cast to enum-array (CS0030).
   - Affected CI errors: `Cannot convert int to NDCertificateFeature[]`
     (PlayReady), `Cannot convert int to DigitalWindowMode[]` (Devices).

2. `IsAnyStruct`: OLD `AbiTypeHelpers.IsAnyStruct(cache, sig)` returned
   true ONLY for blittable structs (it explicitly rejected any struct with
   reference-type fields). The new resolver method returned true for BOTH
   `BlittableStruct` AND `ComplexStruct` shapes.
   - Symptom 1: `StructEnumMarshallerFactory.almostBlittable` was wrongly
     true for complex structs, so `isComplexStruct` became false and the
     marshaller class skipped emitting `ConvertToUnmanaged`,
     `ConvertToManaged`, and `Dispose` -- only `BoxToUnmanaged` and
     `UnboxToManaged` were emitted.
   - Symptom 2: array/span FillArray classifications wrongly treated
     `Span<EnumType>` and `Span<ComplexStruct>` as non-blittable, emitting
     a `CopyToManaged_X` UnsafeAccessor pattern that referenced
     `ABI.Foo.EnumName*` (which doesn't exist for enums).
   - Affected CI errors:
     - `ProfileUsageMarshaller does not contain a definition for
       'ConvertToUnmanaged' / 'ConvertToManaged' / 'Dispose'` (CS0117 + CS1503)
     - `GameControllerSwitchPosition does not exist in
       ABI.Windows.Gaming.Input` (CS0234)

Fix:
- `IsBlittablePrimitive` now returns true for `BlittablePrimitive` OR `Enum`
  shape (matches OLD).
- `IsAnyStruct` now returns true ONLY for `BlittableStruct` shape
  (matches OLD; `ComplexStruct` is excluded). For the few callsites that
  genuinely want both, `IsComplexStruct` is available separately.

Verified all 3 CI semantic errors disappear from the regenerated output:
- PlayReady SupportedFeatures now emits the proper ReceiveArray pattern
  with `uint __retval_length`, `T* __retval_data`, `try`/`UnsafeAccessor`.
- GameControllerSwitchPosition Span now emits the proper `fixed(void*)`
  blittable FillArray pattern.
- ProfileUsageMarshaller now emits all 5 methods (ConvertToUnmanaged,
  ConvertToManaged, Dispose, BoxToUnmanaged, UnboxToManaged).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t in SDK fallback

Two bugs fixed:

1. AbiMethodBodyFactory.RcwCaller: `returnIsAnyStruct` was true for
   both BlittableStruct AND ComplexStruct shapes. The dispatch order in
   the local-declaration switch checks `returnIsAnyStruct` BEFORE
   `returnIsComplexStruct`, so ComplexStruct returns went to the wrong
   branch (`GetBlittableStructAbiType` which returns the PROJECTED type
   for non-mapped structs) instead of `GetAbiStructTypeName` (which
   correctly returns the ABI struct type).

   Symptom: for any complex struct return (struct with TimeSpan/DateTime
   fields, e.g. GpioChangeCount, GpioChangeRecord, AccessListEntry,
   StorePackageUpdateStatus), the writer emitted:
       global::Windows.X.Y.SomeStruct __retval = default;     // PROJECTED
       (delegate*<void*, global::Windows.X.Y.SomeStruct*, ...>)
       return __retval;                                       // no ConvertToManaged!
       global::ABI.Windows.X.Y.SomeStructMarshaller.Dispose(__retval);  // CS1503

   Should be (matches PRE):
       global::ABI.Windows.X.Y.SomeStruct __retval = default; // ABI
       (delegate*<void*, global::ABI.Windows.X.Y.SomeStruct*, ...>)
       return global::ABI.Windows.X.Y.SomeStructMarshaller.ConvertToManaged(__retval);
       global::ABI.Windows.X.Y.SomeStructMarshaller.Dispose(__retval);

   Affected CI errors: CS1503 `cannot convert from 'Windows.X.Y.Foo' to
   'ABI.Windows.X.Y.Foo'` for GpioChangeRecord, GpioChangeCount,
   AccessListEntry, StorePackageUpdateStatus (Devices.Gpio,
   Storage.AccessCache, Services.Store).

2. ProjectionGenerator.Generate: the SDK-mode fallback at line 228 (which
   adds the Windows SDK namespace filters when no Microsoft.Windows.SDK.NET
   reference assembly is present in the input refs) was forgetting to set
   `hasTypesToProject = true`. The generator's main flow then early-exits
   at `if (!processingState.HasTypesToProject) return;`, silently
   producing no output. This blocked WinRT.Sdk.Projection.csproj from
   compiling.

Now WinRT.Sdk.Projection compiles cleanly with 0 errors against the real
SDK 10.0.26100.1 winmds. This is a real CI-style validation that catches
the kinds of semantic errors we missed before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per code review: `IsAnyStruct` returning only blittable structs is
misleading -- the name implies `BlittableStruct or ComplexStruct`.

Refactor:
- Restore `AbiTypeShapeResolver.IsAnyStruct` to its name-suggested
  semantics (`BlittableStruct or ComplexStruct`).
- Add a new `IsBlittableStruct` method that returns true only for
  `BlittableStruct`.
- Bulk-rename all 36 callers of `IsAnyStruct` to `IsBlittableStruct`.
  These callers were ported from OLD `AbiTypeHelpers.IsAnyStruct` which
  was always blittable-only, so this preserves the ported semantics
  while making the code's intent explicit.
- Rename `returnIsAnyStruct` local in `EmitAbiMethodBodyIfSimple` to
  `returnIsBlittableStruct` so the variable name matches its meaning
  and aligns with `returnIsBlittableStruct` in the parallel DoAbi path.

Result: 178 -> 186 byte-identical / 1553. Real CI-style compile of
WinRT.Sdk.Projection still passes 0 errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
M6 (using static ordering): `using static` directives must come AFTER all
normal `using` directives (sorted alphabetically). Fixed in
`Resources/Base/ReferenceInterfaceEntries.cs` so `using static
System.Runtime.InteropServices.ComWrappers;` is last.

R1 (ABI delegate Invoke wrapper indent): The `unboxedValue` local in
`ReferenceImplFactory.WriteReferenceImpl` (non-blittable struct branch
AND class/delegate branch) was dedented because the multiline raw string
boundary forced the typedef name to land at the writer's outer indent
instead of inside the try block. Refactored to pre-build the
projected/ABI type names as strings and use a single multiline raw string
with the names interpolated inline at the correct indent.

R2 (XAML CCW marshaller IID accessor under-indent): In
`AbiClassFactory`, the `[UnsafeAccessor]` IID accessor for generic
default interfaces was emitted via `EmitUnsafeAccessorForDefaultIfaceIfGeneric`
between two multiline raw strings, with the writer's indent set to the
outer namespace level rather than the class body level. Refactored to
use `WriteBlock()` so the accessor lands inside the class body at the
correct indent.

R3 (Vftbl `struct X{` brace packing): In
`AbiInterfaceFactory.WriteVftblStruct`, the struct header was emitted
via `Write` (no trailing newline) followed by `WriteBlock()`, so the
`{` from WriteBlock landed on the same line as the struct declaration.
Changed `Write` to `WriteLine` so the brace lands on its own line.

R5 (`GeneratedInterfaceIIDs.cs` lost member indent): Fields were emitted
without an `IncreaseIndent` context inside the static class body. Added
`IncreaseIndent`/`DecreaseIndent` calls around the emission loop in
`ProjectionGenerator.GeneratedIids` and `WriteInterfaceIidsEnd`.

R6 (extra blank line after namespace `{` opener): `WriteContract` had a
leading `writer.WriteLine();` that doubled up with the namespace block
opener's trailing newline. Removed the redundant leading WriteLine.

Result: 178 -> 244 byte-identical / 1553 (+66). Real CI-style compile of
WinRT.Sdk.Projection still passes 0 errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…XAML projection

Bug: in commit bcafbf6 (Pass 21: IDE0078 pattern-matching simplification),
an automated refactor incorrectly converted

    if (type != GridUnitType.Auto && type != GridUnitType.Pixel && type != GridUnitType.Star)

to

    if (type is not (GridUnitType.Auto or GridUnitType.Pixe)l && type != GridUnitType.Star)

The closing paren was inserted mid-token, splitting 'Pixel' as 'Pixe)l'
and leaving the third comparison hanging. Compiler interpreted 'Pixe)l'
as a positional pattern with property access on a 1-element deconstruction
of GridUnitType (CS1061 / CS8129). Affected both
`Microsoft.UI.Xaml.GridLength.cs` and `Windows.UI.Xaml.GridLength.cs`
addition resources; both fixed to:

    if (type is not (GridUnitType.Auto or GridUnitType.Pixel) && type != GridUnitType.Star)

Coverage gap: bcafbf6's commit message claimed `Validation: all 8 regen
scenarios produce byte-identical output to baseline`, but the validation
script only diffs PRE writer output against POST -- it doesn't compile
either. Embedded resource files (`Resources/Additions/*.cs`) get
copied verbatim into the projection output, so a typo in the resource
file produces matching POST/PRE diffs (both have the typo if PRE was
generated after the bad commit too) AND no compile-time check.

Coverage fix: extend `validate-compile.ps1` to also build
`WinRT.Sdk.Xaml.Projection` (which compiles `Windows.UI.Xaml.GridLength`,
the addition file containing the same typo). Now we get end-to-end
Roslyn compile coverage for both:
- WinRT.Sdk.Projection (Windows.* core types)
- WinRT.Sdk.Xaml.Projection (Windows.UI.Xaml.* types + addition files)

This catches the GridUnitType typo (and would have caught it in CI before
push if run locally). Both projections compile cleanly with this fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the compound condition '(is not (GridUnitType.Auto or GridUnitType.Pixel) && type != GridUnitType.Star)' with a single pattern '(is not (GridUnitType.Auto or GridUnitType.Pixel or GridUnitType.Star))' in GridLength additions for both Microsoft.UI.Xaml and Windows.UI.Xaml to improve readability while preserving the validation logic.
…urce

The C# port of TypedefNameWriter dropped 3 conditions from the OLD C++
namespace-prefix logic at code_writers.h:363-369:

C++ original:
    forceWriteNamespace ||
    typeNamespace != _current_namespace ||
    (Projected && (InAbiNamespace || InAbiImplNamespace)) ||
    (ABI && !InAbiNamespace) ||
    (EventSource && !InAbiNamespace) ||
    (CCW && authoredType && !InAbiImplNamespace) ||
    (CCW && !authoredType && (InAbiNamespace || InAbiImplNamespace))

C# (broken):
    forceWriteNamespace ||
    typeNamespace != context.CurrentNamespace ||
    (Projected && (InAbiNamespace || InAbiImplNamespace)) ||
    nameToWrite == TypedefNameType.ABI ||                         // missing !InAbiNamespace
    nameToWrite == TypedefNameType.EventSource ||                 // missing !InAbiNamespace
    (nameToWrite == TypedefNameType.CCW && authoredType)          // missing !InAbiImplNamespace
                                                                  // missing entire (CCW && !authoredType && ...) clause

The (CCW && authoredType && !InAbiImplNamespace) gap caused the writer
to emit interface DECLARATIONS like:

    internal interface global::ABI.Impl.AuthoringTest.IBasicClassClass

instead of the unqualified:

    internal interface IBasicClassClass

Compiler interprets `interface global::Foo.Bar` as a malformed
declaration: CS1514 `{ expected`, CS1513 `}` expected, CS1001
`Identifier expected`. Hit in the AuthoringTest scenario (component
mode where ALL types are emitted inside `namespace ABI.Impl.<ns>` so
`InAbiImplNamespace == true` for every typedef-name write).

Restored all 4 conditions verbatim from the C++ source. Coverage gap
addressed in a follow-up commit (validate-compile.ps1 will be extended
to do syntax-only validation across all scenarios so CS1xxx errors are
caught locally before push).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Sergio0694 Sergio0694 added code cleanup Code cleanup and refactoring tooling CsWinRT 3.0 labels May 12, 2026
@Sergio0694 Sergio0694 requested a review from manodasanW May 12, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code cleanup Code cleanup and refactoring CsWinRT 3.0 tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant