Refactor projection writer: interpolated-string + callback emission, classifier resolvers, helper consolidation#2419
Open
Sergio0694 wants to merge 199 commits into
Conversation
Introduce AppendInterpolatedStringHandler (ref struct) to support compiler-backed interpolated-string appends into IndentedTextWriter. The new handler (internal, EditorBrowsable(Never), [InterpolatedStringHandler]) handles literals, formatted values (with optional format), ReadOnlySpan<char>, and preserves multiline semantics (CRLF -> LF normalization and per-line indentation); it leverages StringBuilder's handler for non-string formatting to avoid allocations. Also make IndentedTextWriter partial, add the necessary System.Runtime.CompilerServices import, and add two Write overloads annotated with [InterpolatedStringHandlerArgument] so the compiler can bind interpolated strings to the new handler.
Rework IndentedTextWriter surface and internals: reorder Write/WriteIf/WriteLine overloads to take the isMultiline flag first, add convenience overloads for string and ReadOnlySpan<char>, and expose interpolated-string handler overloads for Write/WriteLine. Tighten behaviour: early-return on empty spans, improve newline insertion when previous buffer ends with '{' or '}', and centralize indentation handling in WriteRawText. Also update AppendInterpolatedStringHandler to accept a scoped ReadOnlySpan<char>, remove ToStringAndClear, and ensure Clear resets indentation. Includes XML doc/remarks linking and small comment/formatting tweaks.
Pass _isMultiline as the first argument to _writer.Write across IndentedTextWriter.AppendInterpolatedStringHandler. Updated calls in AppendLiteral and the various AppendFormatted overloads to match the expected Write(bool, ...) signature and avoid swapping the value and boolean parameters.
The recent IndentedTextWriter refactor moved the `isMultiline` parameter
to the FIRST positional position on the new Write/WriteLine overloads
(to satisfy the InterpolatedStringHandlerArgument requirement that the
named handler argument come after its referenced parameters).
This commit updates all 231 callsites across 25 files in the
`WinRT.Projection.Writer` to match. Mechanical change:
writer.WriteLine($$\"\"\"
...content...
\"\"\", isMultiline: true);
becomes:
writer.WriteLine(isMultiline: true, $$\"\"\"
...content...
\"\"\");
No semantic differences. Build passes with 0 warnings, 0 errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eIf pattern The two `WriteLineIf` content overloads still had `bool isMultiline = false` as a trailing parameter with a default value -- a leftover from before the `isMultiline`-first refactor. The other overload families (`Write`/`WriteLine`/`WriteIf`) all use a pair-of-overloads pattern where each content overload has a sibling that takes `isMultiline` immediately before the content (no default). Reshape `WriteLineIf` to match. The `WriteLineIf(bool condition, bool skipIfPresent = false)` newline-only overload remains unchanged (analogous to `WriteLine(bool skipIfPresent = false)`). The two content overloads each gain a sibling: - `WriteLineIf(bool condition, string content)` - `WriteLineIf(bool condition, bool isMultiline, string content)` (new shape) - `WriteLineIf(bool condition, ReadOnlySpan<char> content)` - `WriteLineIf(bool condition, bool isMultiline, ReadOnlySpan<char> content)` (new shape) No callers needed updating: `WriteLineIf` has zero external callers in the writer codebase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce two constructor overloads for IndentedTextWriter.AppendInterpolatedStringHandler to support compiler-generated conditional interpolated string emission. Both overloads accept literalLength, formattedCount, writer, condition and an out bool shouldAppend; one overload also accepts an isMultiline parameter. When condition is false the handler is disabled (shouldAppend = false), _writer is set to null! and _isMultiline is set to false; when true the writer and multiline flag are initialized. XML docs note these are intended for compiler use and arguments are not validated.
Combines the conditional-style docs from the existing string/span `WriteIf` and `WriteLineIf` overloads with the interpolated-handler-style docs from the existing `Write` and `WriteLine` handler overloads, matching the documentation pattern used everywhere else in the file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mechanical conversion across the writer codebase: any single-statement
`if (cond) { writer.Write[Line](content); }` block where the body is a
plain (non-interpolated) string or span call is rewritten to
`writer.Write[Line]If(cond, content);`. 68 conversions across 23 files.
The conversion intentionally SKIPS interpolated-string calls
(`writer.Write($"...")`). The new `WriteIf`/`WriteLineIf` interpolated
handler overloads have an unintentional parameter-name mismatch with the
existing `AppendInterpolatedStringHandler` constructors: the compiler
binds `condition` positionally to the handler's existing
`(int, int, IndentedTextWriter, bool isMultiline)` constructor, so
`writer.WriteLineIf(false, $"foo")` would silently always write AND
treat the content as multiline. Once the handler gets a dedicated
`(..., bool condition)` constructor that returns `out bool wantsToContinue`
this filter can be relaxed.
Conditions skipped by the converter:
- if-statement is part of an `else`/`else if` chain (preceded by `else`
on the prior non-blank/non-comment line, OR followed by `else` on the
next non-blank/non-comment line after the close brace -- the previous
filter only checked the immediately-following line and missed cases
separated by a blank line + comment).
- body uses an interpolated string literal or an `isMultiline:` named
argument (which would route to the interpolated handler overload).
- body has more than one statement, or body's call doesn't end with `);`.
Validation:
- Output is byte-identical to the pre-conversion branch baseline (1553/1553).
- Real CI-style compile of WinRT.Sdk.Projection + WinRT.Sdk.Xaml.Projection
passes with 0 compile errors.
- Syntax validation across all 8 regression scenarios passes with 0 errors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce a separate interpolated-string handler type for conditional write paths (WriteIf/WriteLineIf). It mirrors the shape and members of AppendInterpolatedStringHandler but only exposes the constructors that take a leading 'condition' argument and produce 'out bool shouldAppend', which is the contract the language compiler requires to short-circuit literal/interpolation evaluation when the condition is false. Splitting this into its own handler type avoids the constructor-overload collision with the unconditional Write/WriteLine overloads: per the interpolated-string-handler resolution rules, the compiler always prefers a constructor with a trailing 'out bool shouldAppend' parameter when one exists with a matching shape, which would otherwise silently bind unconditional 'Write(bool isMultiline, ...)' calls to the conditional constructor and lose the isMultiline value. This commit only adds the new type. Wiring WriteIf/WriteLineIf to it (and removing the now-redundant conditional constructors from AppendInterpolatedStringHandler) follows in a separate commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the four interpolated-handler overloads on WriteIf/WriteLineIf to take a 'ref TryAppendInterpolatedStringHandler' parameter instead of 'ref AppendInterpolatedStringHandler', and remove the now-unused conditional constructors from AppendInterpolatedStringHandler. This fixes a latent bug introduced when the conditional constructors were originally added to AppendInterpolatedStringHandler. Per the language rules for interpolated-string-handler resolution, the compiler always prefers a constructor with a trailing 'out bool shouldAppend' parameter when one exists with a matching shape. Both WriteIf(bool condition, ...) and Write(bool isMultiline, ...) produce the same expected handler-constructor signature (int, int, IndentedTextWriter, bool) from their respective [InterpolatedStringHandlerArgument] attributes, so the addition of the 5/6-arg conditional constructors silently caused all Write(isMultiline: true, $$..."""...""") calls to bind to the conditional constructor, where the bool parameter was treated as a condition flag and _isMultiline was unconditionally forced to false. As a result, the AppendLiteral path stopped splitting multi-line raw-string content per line and lost the per-line indent contribution, producing visibly broken indentation inside generated projection sources wherever an interpolated multi-line raw string was written. Splitting the conditional flavor into its own handler type breaks the constructor-shape collision: WriteIf/WriteLineIf still get the short-circuit behavior they need, while Write/WriteLine bind to the AppendInterpolatedStringHandler 4-arg (int, int, writer, bool) constructor as intended. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduce a small "callback" mechanism that lets factories return a readonly value-type closure which can be embedded as an interpolation hole inside an interpolated raw string passed to Write/WriteLine. The callback's Write method is invoked at interpolation time and emits its content directly into the writer at the writer's current position and indentation level, with no allocation and no intermediate string. This lets emission code that previously had to break a single multiline fragment into three separate calls (a leading writer.Write of the prefix, a helper that emits a piece of derived content, and a trailing writer.WriteLine of the suffix) collapse back into a single, readable interpolated raw string with the helper inlined as an interpolation hole, while preserving the same output and the same zero-allocation characteristics. The new pieces are: - IIndentedTextWriterCallback: marker interface implemented by the callback closures. - WriteIidGuidReferenceCallback: the first concrete callback, emitted by the new AbiTypeHelpers.WriteIidGuidReference(context, type) overload to inline the IID GUID literal expression. - AppendFormatted<T> dispatch on both interpolated-string handlers (AppendInterpolatedStringHandler and TryAppendInterpolatedStringHandler): when T is a value type that implements IIndentedTextWriterCallback, the handler invokes the callback's Write method directly instead of falling through to the string/StringBuilder path. Use the new pattern in AbiInterfaceFactory in the two places that previously emitted the IID via a three-call sequence (the static IID property body and the inline-IID argument in the marshaller emission). Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
Add an extension method that, given any value-type implementation of IIndentedTextWriterCallback, leases an IndentedTextWriter from the pool, invokes the callback against it, and returns the resulting string. Use this overload when the caller needs the emitted text as a standalone string (e.g. to compose into another string or pass through APIs that take string), rather than appending it inline as an interpolation hole inside a larger writer call. Currently unused; subsequent commits will introduce additional callbacks and migrate existing string-returning helpers to call factory(...).Format() instead of duplicating the leased-pool boilerplate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lers
Add two new IIndentedTextWriterCallback implementations that mirror the
existing writer-taking factory methods on TypedefNameWriter:
- WriteTypedefNameCallback wraps WriteTypedefName(IndentedTextWriter,
ProjectionEmitContext, TypeDefinition, TypedefNameType, bool).
- WriteTypeParamsCallback wraps WriteTypeParams(IndentedTextWriter,
TypeDefinition).
Each comes with a callback-returning factory method on TypedefNameWriter
that uses '<inheritdoc>' on the corresponding writer-taking factory so
the doc surface stays in one place. The existing callback-returning
WriteIidGuidReference factory on AbiTypeHelpers gets the same
'<inheritdoc>' treatment for consistency.
Adding the callback-returning WriteTypedefName overload at the same
signature as the existing string-returning helper would conflict on
return type (C# does not allow return-type-only overloads), so the
string-returning WriteTypedefName(ProjectionEmitContext, ...) overload
is removed and its 8 callers are migrated to call the new factory and
invoke the IIndentedTextWriterCallback.Format() extension method to
get the standalone string. The dead StartsWith(GlobalPrefix) defensive
checks at each call site are also dropped: every caller passed
'forceWriteNamespace: true', and the writer-taking method always
emits the 'global::' prefix when that flag is set, so the defensive
fallback branch was unreachable.
Output is byte-identical for all 10 generated files in the
refgen-pushnot scenario (validated against the post-handler-fix
baseline).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The marshaller emission previously broke a single coherent class
declaration into nine separate writer calls (six string literals plus
five WriteTypedefName/WriteTypeParams pairs) so it could splice the
projected type name into multiple positions inside the body. Each
literal segment had to manually carry its own interior indentation,
which made the layout fragile and hid the underlying shape of the
emitted class.
Replace the chunked emission with a single multiline interpolated raw
string. The callbacks for the three repeated holes are constructed
once as locals and embedded inline:
- WriteTypedefNameCallback for the projected type name (used 4 times)
- WriteTypeParamsCallback for the generic parameter list (used 4 times)
- WriteIidGuidReferenceCallback for the IID GUID literal (used 1 time)
The new form preserves the existing zero-allocation behavior (the
callbacks dispatch through IIndentedTextWriterCallback at interpolation
time) while making the emitted class shape readable at the call site.
Side effect: this also fixes a latent indent bug in the previous
chunked emission. The non-isMultiline 'writer.Write($$..."""...""")'
call between the inner WriteTypedefName/WriteTypeParams pair and the
trailing 'public static ...' segment was emitting its literal interior
as raw text, which prepended only the writer's class-level indent
(4 spaces) to the closing '}' of ConvertToUnmanaged and to the next
'public static' declaration, leaving them at column 4 while the
matching opening '{' of ConvertToManaged ended up at column 8. The
single multiline raw string with isMultiline routing fixes the
indentation so every member inside the class is uniformly at column 8.
Output diffs against the post-handler-fix baseline are limited to the
four files containing non-exclusive non-generic projected interfaces
(TestComponent.cs, TestComponentCSharp.cs,
TestComponentCSharp.TestPublicExclusiveTo.cs,
TestComponentCSharp.Windows.cs); each diff is the indent-fix described
above. The generated sources still parse cleanly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename the interpolated string handler type and file from TryAppendInterpolatedStringHandler to AppendIfInterpolatedStringHandler, updating constructor names and all references in IndentedTextWriter (WriteIf/WriteLineIf signatures). Also apply a tiny formatting tweak in IIndentedTextWriterCallbackExtensions.cs (spacing around callback.Write). This aligns the handler name with its semantics and keeps usages consistent.
Add a polymorphic dispatch path to AppendFormatted<T> on both the AppendInterpolatedStringHandler and AppendIfInterpolatedStringHandler so callers can declare a local of type IIndentedTextWriterCallback (rather than a concrete callback struct) and assign one of several concrete callback values based on a runtime condition. The local can then be embedded as an interpolation hole in the same way the existing value-type callbacks are. The existing 'typeof(T).IsValueType && value is IIndentedTextWriterCallback' fast path is preserved unchanged: when 'T' is a concrete callback struct (the common case) the JIT can constant-fold the type checks and elide the polymorphic branch entirely. The new branch only fires when 'T' is the interface type itself (or some other reference type that happens to implement the interface), which is the case where the caller wanted polymorphic behavior in the first place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ng callers Add the missing writer-taking method 'WriteTypedefNameWithTypeParams( IndentedTextWriter, ProjectionEmitContext, TypeDefinition, TypedefNameType, bool)' on TypedefNameWriter, plus a matching WriteTypedefNameWithTypeParamsCallback struct and a callback-returning factory overload that uses '<inheritdoc>' on the writer-taking method to keep the doc surface in one place. The writer-taking method is a tiny composition of 'WriteTypedefName' and 'WriteTypeParams' (which is exactly what the previous string-returning convenience already did internally) and lets callers emit the typedef-name + generic-parameter list as a single inline operation on a writer. The callback factory wraps it for use as an interpolation hole inside a larger interpolated raw string, mirroring the WriteTypedefName / WriteTypeParams / WriteIidGuidReference pattern. The callback-returning factory occupies the same signature as the previous string-returning 'WriteTypedefNameWithTypeParams' overload, so the string-returning version is removed and its 6 callers (in ComponentFactory, IidExpressionGenerator, MetadataAttributeFactory) are migrated to call the new factory and chain '.Format()' to get the standalone string. Output is byte-identical for all 10 generated files in the refgen-pushnot scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the chunked '[opener]; WriteTypedefName; WriteTypeParams;
[closer]' emission pattern in five attribute writers with single
interpolated raw strings that embed the typedef-name + generic-params
emission as interpolation holes (using
WriteTypedefNameWithTypeParamsCallback and the existing WriteIid /
WriteTypedefName / WriteTypeParams callbacks). The resulting attribute
text is now legible at the call site, and conditional pieces inside
the template are prepared as locals before the writer call so the
'$$"""..."""' block stays whole.
The methods updated are:
- WriteWinRTMetadataTypeNameAttribute, WriteWinRTMappedTypeAttribute,
WriteWinRTReferenceTypeAttribute: simple one-liners now built as
a single interpolated WriteLine.
- WriteWinRTWindowsMetadataTypeMapGroupAssemblyAttribute and
WriteWinRTComWrappersTypeMapGroupAssemblyAttribute: the 'target:'
typeof slot toggles between the ABI typedef and the projected
typedef based on 'context.Settings.Component'; both branches
yield WriteTypedefNameWithTypeParamsCallback values that get
selected into a value-typed local. The 'value:' string-literal
slot in the ComWrappers attribute toggles between an
'IReference`1<...>' wrapper and the bare projection name; both
branches yield strings, so a plain 'string' local is used.
- WriteWinRTIdicTypeMapGroupAssemblyAttribute: two distinct
callbacks (Projected source, ABI proxy) prepared as locals and
embedded in a single interpolated raw string.
Output is byte-identical for all 10 generated files in the
refgen-pushnot scenario; the generated sources still parse cleanly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both EmitDicShimIObservableMapForwarders and EmitDicShimIObservableVectorForwarders previously split their emission into two separate WriteLine(isMultiline: true, $$..."""...""") calls because the second block (the IObservableMap.MapChanged / IObservableVector.VectorChanged event forwarder) needed locals (obsTarget, obsSelf) that weren't available when the first block was emitted. Lift those locals to the top of each method alongside the existing target / self / icoll locals so a single interpolated multiline raw string can carry both blocks separated by a blank line. The output is byte-identical (validated against the post-handler-fix baseline for all 10 generated files in the refgen-pushnot scenario). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the two-line 'writer.Write("...prefix..."); WriteTypedefName;
WriteTypeParams; writer.WriteLine()' sequences in
AbiInterfaceIDicFactory.WriteIdicFileInterfaceDecl and
ClassFactory.WriteStaticClass with a single interpolated
'writer.WriteLine($"...{{name}}")' call that embeds a
WriteTypedefNameWithTypeParamsCallback local as the interpolation hole.
Output is byte-identical for all 10 generated files in the
refgen-pushnot scenario.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…allbacks
Hoist 'WriteTypedefNameCallback' locals for the ABI and projected
typedef names at the top of the 'isComplexStruct' branch and use
single multiline interpolated raw strings for each of the three
method signatures emitted by that branch:
- ConvertToUnmanaged({{projected}} value) returning {{abi}}
- ConvertToManaged({{abi}} value) returning {{projected}}
- Dispose({{abi}} value)
The previous emission split each signature across four to six
separate writer calls (Write prefix; WriteTypedefName ABI;
Write " ConvertToUnmanaged("; WriteTypedefName Projected;
WriteLine multiline body opener) so the typedef-name pieces could be
spliced into a single declaration line. The new form puts each
signature (and its body opener) into one readable multiline raw
string with the callback locals embedded as interpolation holes.
The 'new {{projected}}{{(useObjectInitializer ? "(){" : "(")}}'
hole inlines the existing conditional-suffix choice so the
'return new ...' line stays inside the same raw string as the rest
of the body opener instead of being emitted by a follow-up
'writer.WriteLine(useObjectInitializer ? "(){" : "(")' call.
Output is byte-identical for all 10 generated files in the
refgen-pushnot scenario.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…llbacks
Hoist the per-type 'abi' and 'projected' WriteTypedefNameCallback
locals to the top of WriteStructEnumMarshallerClass so the
BoxToUnmanaged, UnboxToManaged, and CreateObject emission paths can
share them, then collapse the chunked
'writer.Write([prefix]); WriteTypedefName; writer.Write([infix]);
WriteIidReferenceExpression; writer.WriteLine([suffix])' sequences
in each emitter into single interpolated multiline raw strings.
- BoxToUnmanaged: the two prior branches (enum/almost/complex
vs. mapped struct) had identical layout differing only in the
CreateComInterfaceFlags value. The single shared template now
selects the flag string via the existing 'hasReferenceFields'
condition. The string-returning
'ObjRefNameGenerator.WriteIidReferenceExpression(TypeDefinition)'
overload supplies the IID hole as a plain string.
- UnboxToManaged: the prior 'isEnum || almostBlittable' and 'else'
(mapped struct) branches emitted identical bodies (UnboxToManaged
of the projected type), so they collapse into one branch. The
remaining 'isComplexStruct' branch becomes a single multiline raw
string with '{{projected}}' and '{{abi}}' interpolation holes.
- CreateObject: each of the two return-statement emitters becomes a
single 'writer.WriteLine($"...{{name}}...")'. The complex-struct
branch uses a per-call 'abiFq' callback because that site needs
'forceWriteNamespace: true' (whereas the shared 'abi' local uses
'false').
Side effect: this also fixes a latent indent bug in the complex-struct
UnboxToManaged branch. The previous chunked emission wrote a multiline
opener followed by 'WriteTypedefName(ABI)' + 'writer.Write("? abi =
...")' on what should have been the next line; the writer indent was
applied to the multiline lines but not to the typedef-name-then-Write
segment, leaving the 'Nested? abi = ...' line at column 4 while the
following 'return ...' line was at column 12. The new single multiline
raw string aligns both lines to column 12 via uniform writer indent
handling. The change is purely whitespace (verified with a Roslyn
syntax check) and affects three generated files (Microsoft.Windows.
PushNotifications.cs, TestComponent.cs, TestComponentCSharp.cs).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ations
Add a 1:1 callback wrapper for
'InterfaceFactory.WriteTypeInheritance(IndentedTextWriter, ProjectionEmitContext,
TypeDefinition, bool, bool)' (the writer-taking method that emits the
' : Base, Iface1, Iface2<T>' clause for class and interface declarations),
plus a paired callback-returning factory overload with '<inheritdoc>'
on the writer-taking method.
Use the new callback to collapse the chunked
'writer.Write("...prefix..."); WriteTypedefName; WriteTypeParams;
WriteTypeInheritance; writer.WriteLine()' sequences at the two existing
class/interface declaration call sites into single interpolated
'writer.WriteLine($"...{{name}}{{inheritance}}")' calls:
- InterfaceFactory.WriteInterfaceDeclaration emits
'{accessibility} interface {{name}}{{inheritance}}'.
- ClassFactory.WriteClassDeclaration emits
'{accessibility} {modifiers}class {{name}}{{inheritance}}', with
the trivial 'static '/'sealed ' modifier choice inlined as a
'string modifiers' local (rather than going through the
'WriteClassModifiers' writer helper, which still exists for any
future caller that needs the writer-side form).
Output is byte-identical for all 10 generated files in the
refgen-pushnot scenario.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Collapse the five-call emission sequence in the blittable / blittable- struct branch of WriteReferenceImpl get_Value (Write opener; WriteTypedefName Projected; Write infix; WriteTypedefName Projected again; WriteLine closer) into a single interpolated multiline raw string with a hoisted WriteTypedefNameCallback local embedded as the type-name interpolation hole in both the cast and the pointer cast. Output is byte-identical for all 10 generated files in the refgen-pushnot scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…callers Add a 1:1 callback wrapper for 'TypedefNameWriter.WriteTypeName(IndentedTextWriter, ProjectionEmitContext, TypeSemantics, TypedefNameType, bool)' plus a paired callback-returning factory overload with '<inheritdoc>' on the writer-taking method. Adding the callback-returning overload at the same signature as the existing string-returning 'WriteTypeName(context, semantics, ...)' helper would conflict on return type, so the string-returning overload is removed and its 11 callers (in AbiClassFactory, AbiInterfaceIDicFactory, AbiMethodBodyFactory.MethodsClass, ClassMembersFactory.WriteInterfaceMembers, MappedInterfaceStubFactory, MetadataAttributeFactory, ObjRefNameGenerator) are migrated to call the new factory and chain '.Format()' to get the standalone string. Output is byte-identical for all 10 generated files in the refgen-pushnot scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…callbacks
Collapse the 'writer.Write(prefix); TypedefNameWriter.WriteTypeName(writer,
ctx, semantics, ...); writer.Write[Line](suffix)' three-call sequences in
two emit sites into single interpolated 'writer.WriteLine($"...{name}...")'
calls with a hoisted WriteTypeNameCallback local as the interpolation hole:
- EventTableFactory.EmitDoAbiAddEvent (non-generic branch) emits
' var __handler = {{abiTypeName}}Marshaller.ConvertToManaged({{handlerRef}});'.
- ComponentFactory.WriteFactoryMethodParameters (typed-parameter branch)
emits '{{projectedType}} {{paramName}}'.
Output is byte-identical for all 10 generated files in the
refgen-pushnot scenario.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the two highest-leverage method-signature callbacks identified by the multi-agent codebase analysis (M1+M2). They pair naturally because 8 of the 10 affected call sites use both callbacks back-to-back to emit a single method declaration. Migrated sites: - AbiDelegateFactory.cs:174 (Invoke extension) - AbiInterfaceIDicFactory.cs:302, 435 (DIM thunks) - AbiMethodBodyFactory.MethodsClass.cs:65 (Methods class entry) - ClassFactory.cs:358 (static-class method emission) - ClassMembersFactory.WriteInterfaceMembers.cs:315 (UnsafeAccessor extern, return-type only), :328 (generic-iface wrapper), :355 (non-generic wrapper), :383 (overridable explicit-iface impl) - InterfaceFactory.cs:242 (interface member declaration) Output is byte-identical for all 10 generated files in the refgen-pushnot scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror the established WriteTypedefName / WriteTypeName migration pattern for ObjRefNameGenerator.WriteIidExpression: add a 1:1 callback wrapper, expose a same-named callback-returning factory overload (with <inheritdoc> on the writer-taking method), remove the now- conflicting string-returning convenience overload, and migrate its 7 string-callers to call the factory and chain '.Format()'. Also rewrite the 2 inline writer-taking call sites in ClassFactory.cs (the GetActivationFactory return statement at :541-555 and the IsOverridableInterface OR-chain at :732-734) to use the callback as an interpolation hole, collapsing each chunked emission into a single interpolated raw string. Output is byte-identical for all 10 generated files in the refgen-pushnot scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e callers Same pattern as M3 (WriteIidExpressionCallback) but for the IReference<T>-flavored IID variant on ObjRefNameGenerator. Add the 1:1 callback wrapper, expose the same-named factory overload (with <inheritdoc>), remove the string-returning convenience overload, and update the 4 callers in AbiDelegateFactory.cs:199,413 and StructEnumMarshallerFactory.cs:257,301 to declare the local as WriteIidReferenceExpressionCallback. The local is consumed only via interpolated raw strings on the writer at all 4 sites, so no template changes are needed. Output is byte-identical for all 10 generated files in the refgen-pushnot scenario. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a single WriteEventTypeCallback struct that carries the nullable
'currentInstance' parameter (the only difference between the two
writer-taking overloads, since the 3-arg overload simply delegates to
the 4-arg one with null). Expose two same-named factory overloads on
TypedefNameWriter (with <inheritdoc> on the corresponding writer-taking
methods), one per shape.
Remove the conflicting string-returning 'WriteEventType(context, evt)'
overload and migrate its single caller (EventTableFactory.cs:29) to
declare a WriteEventTypeCallback local that gets interpolated directly
into the surrounding multiline raw string.
Refactor the 5 inline writer-taking sites in AbiInterfaceIDicFactory.cs:351,515,
ClassFactory.cs:387, ClassMembersFactory.WriteInterfaceMembers.cs:568,
InterfaceFactory.cs:269 to use the callback as a {{eventType}}
interpolation hole, collapsing each chunked emission into a single
interpolated raw string per event declaration.
Output is byte-identical for all 10 generated files in the
refgen-pushnot scenario.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ccessor Commit 490787a ('Reference-projection events & static factory') refactored WriteStaticFactoryObjRef from a single-block emitter that always opened the property body with 'get { ... }' into two separate code paths (reference projection vs non-reference). The non-reference path lost the 'get' keyword while keeping the property's outer '{ ... }' braces, so the emitted property body became a bare block: private static WindowsRuntimeObjectReference _objRef_X { var __X = field; // <- CS1014 here if (...) return __X; return field = WindowsRuntimeObjectReference.GetActivationFactory(...); } This is what the failing CI was reporting as 'CS1014: A get or set accessor expected' across many of the per-namespace projection files (Windows.Management.Update.cs, Windows.ApplicationModel.Background.cs, Windows.AI.MachineLearning.cs, etc.). Restore the 'get { ... }' accessor wrapper around the body so the generated property compiles. The reference-projection branch (added in 490787a) is unaffected because it uses an expression-bodied property ('=> throw null;') which doesn't need accessor braces. Verified locally: build succeeds with 0 warnings; the validation harness reports the expected ~640 diffs across pushnot / everything-with-ui / windows scenarios - every single one of which is exactly the missing 'get' accessor being added back to a static factory objref property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…in the projection writer
Sweep the projection writer for unnecessary null checks on the metadata cache:
- ProjectionEmitContext.Cache is non-nullable (MetadataCache, not MetadataCache?) and is set from the non-nullable constructor parameter; the various 'MetadataCache cache' parameter sites are non-nullable too. The 'cache is null' / 'cache is not null' / 'context.Cache is null' / 'context.Cache is not null' guards were defensive dead branches.
- Also drop the 'MetadataCache cache = context.Cache;' local-variable stashes (in WriteFactoryClass and AbiInterfaceFactory) and just use 'context.Cache' directly at the few call sites; the locals were holding zero state and only obscured the call.
Sites cleaned up:
- AbiTypeHelpers.AbiTypeNames.cs: 'cache is null ? "int" : GetProjectedEnumName(def)' -> 'GetProjectedEnumName(def)'.
- AbiTypeHelpers.Blittability.cs (IsRuntimeClassOrInterface): unwrapped the 'if (cache is not null)' guard around the cross-module resolve.
- AbiTypeWriter.cs (Reference case): unwrapped the 'if (context.Cache is not null)' guard; dedented the body; deduped 'var (rns, rname) = r.Type.Names()' (was redeclared inside the now-removed inner scope and inside 'if (r.IsValueType)').
- ClassFactory.WriteStaticClassMembers: dropped the 'if (context.Cache is null) return;' early-out.
- ClassFactory (ref-mode synthetic ctor block): 'else if (context.Cache is not null)' -> 'else'.
- ClassMembersFactory.WriteInterfaceTypeNameForCcw: dropped the '&& context.Cache is not null' conjunct; promoted the multi-type negation to a single 'is not (TypeDefinition or TypeSpecification)' pattern that the strict analyzer prefers.
- ComponentFactory.WriteFactoryClass: removed the 'MetadataCache cache = context.Cache' local and unwrapped the 'if (cache is not null)' block around the factory-member emission loop.
- ConstructorFactory.WriteAttributedTypes: dropped the 'if (context.Cache is null) return;' early-out.
- MetadataAttributeFactory: four sites - two pattern-match cleanups ('is not TypeDefinition && is not TypeSpecification' -> 'is not (TypeDefinition or TypeSpecification)') and two compound-assignment cleanups ('if (ifaceDef is null) { ifaceDef = ... }' -> 'ifaceDef ??= ...').
- SignatureGenerator (Reference + GenericInstanceRef cases): unwrapped the 'if (context.Cache is not null)' guards; the cache-resolved 'TryResolve(...) ?? Find(...)' expression now runs unconditionally.
- AbiInterfaceFactory (component-mode exclusive-to resolution): removed the 'MetadataCache cache = context.Cache' local; the two cache uses now reference context.Cache directly.
Build clean (0 warnings); validation harness reports the same ~640 diffs as the previous fix commit (the local baseline cache was captured against the get-less output and hasn't been refreshed yet) - this commit adds 0 new behavioral diffs, confirmed by sampling: every diff is still just the 'get { ... }' restoration from f21c8d3.
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
…ace-based callback model Foundation step for switching the writer's callback model from a "one struct per emission helper that implements IIndentedTextWriterCallback" pattern to a single 'IndentedTextWriterCallback' delegate type. Add: - 'Writers/IndentedTextWriterCallback.cs': the new 'internal delegate void IndentedTextWriterCallback(IndentedTextWriter writer)'. This is what factory helpers will return going forward, and what local emission lambdas and local functions will be assignable to. Update: - 'IndentedTextWriter.AppendInterpolatedStringHandler.AppendFormatted<T>': add a 'value is IndentedTextWriterCallback writeCallback' branch alongside the existing struct- and interface-dispatch paths so an interpolation hole that contains a delegate just invokes it. - 'IndentedTextWriter.AppendIfInterpolatedStringHandler.AppendFormatted<T>': same delegate branch. - 'IIndentedTextWriterCallbackExtensions': add a 'Format(this IndentedTextWriterCallback)' overload alongside the existing 'Format<T>(this T)' struct overload, so the standalone "render to string" path works for delegates too. The legacy 'IIndentedTextWriterCallback' interface and the 37 'WriteXxxCallback' structs are unchanged in this commit - existing factory methods and call sites continue to work exactly as before. The migration to delegates and the deletion of all the struct callbacks comes in the follow-up commit. Build clean (0 warnings); validation harness reports the same ~640 baseline-staleness diffs as the last few commits (all attributable to the prior get-accessor fix - this commit adds 0 new behavioral diffs). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…edTextWriterCallback delegate
Replace every per-emission-helper callback struct (37 of them) with a single delegate-based model:
- Migrate all 'WriteXxxCallback' / 'EmitXxxCallback' factory methods. Each one previously returned a custom 'XxxCallback' struct and dispatched through 'IIndentedTextWriterCallback.Write'; they now all return the 'IndentedTextWriterCallback' delegate (added in the previous commit) via a one-liner lambda:
// Before
public static WriteFooCallback WriteFoo(Foo foo, Bar bar) => new(foo, bar);
// After
public static IndentedTextWriterCallback WriteFoo(Foo foo, Bar bar)
{
return writer => FooFactory.WriteFoo(writer, foo, bar);
}
- Update every call site that locally typed the callback (e.g. 'WriteFooCallback foo = ...;') to use 'IndentedTextWriterCallback' instead.
- Delete the entire 'src/WinRT.Projection.Writer/Factories/Callbacks/' folder (37 struct files).
- Delete 'Writers/IIndentedTextWriterCallback.cs' (no more interface).
- Strip the legacy 'Format<T>(this T) where T : struct, IIndentedTextWriterCallback' overload from the extensions class (rename the file to 'IndentedTextWriterCallbackExtensions.cs' / class to 'IndentedTextWriterCallbackExtensions') leaving only the delegate-based 'Format(this IndentedTextWriterCallback)' overload.
- Simplify both 'IndentedTextWriter.AppendInterpolatedStringHandler.AppendFormatted<T>' and 'AppendIfInterpolatedStringHandler.AppendFormatted<T>': drop the value-type-fast-path and interface-polymorphic-fallback dispatches; the delegate check is the only callback-dispatch path that's needed now.
- Sweep all 29 files that previously had 'using WindowsRuntime.ProjectionWriter.Factories.Callbacks;' and remove that import.
Motivation: the per-callback struct model was forcing every emission helper that wanted to be usable as an interpolation hole to grow a dedicated companion struct + corresponding factory method, which made the writer noticeably more verbose than its C++ ancestor and made it impossible to drop in a local function as an emitter without first defining yet another struct. The new model lets callers write 'IndentedTextWriterCallback foo = w => { ... };' or pass a local function group reference and it just works, while keeping the existing 'XxxFactory.WriteXxx(...)'-returning-a-callback pattern working with a trivial lambda forward.
Stats: 70 files changed, +235 / -1133 (a net deletion of ~900 lines, almost entirely the 37 struct boilerplate files).
Build clean (0 warnings); 'dotnet publish' of WinRT.Projection.Generator completes Native AOT codegen with no warnings; validation harness still reports the same ~640 baseline-staleness diffs from the prior get-accessor fix - 0 new behavioral diffs introduced by this commit, confirmed by sampling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Allow method-group conversions by accepting Action<IndentedTextWriter> values in both AppendIfInterpolatedStringHandler and AppendInterpolatedStringHandler. When such an Action is passed, invoke it against the underlying writer and return early. Also minor comment cleanup near the callback handling.
Refactor how activation factory classes are emitted by switching from ad-hoc string/list building to small writer callbacks and a templated writer block. Added local helper lambdas (WriteBaseInterfaceList, WriteActivateInstanceBody) and a new private method WriteAdditionalActivationFactoryMethods to emit static/constructor wrappers. Simplified iteration over AttributedTypes (tuple deconstruction), removed the interim factoryInterfaces list, and updated delegate returns to call local helpers. Also added a pragma to suppress IDE0061 and minor formatting/whitespace cleanups. These changes make the factory emission logic clearer and easier to extend. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
Simplify many IndentedTextWriterCallback factory methods across the project by replacing qualified calls like "return writer => TypeName.Method(writer, ...)" with unqualified "return writer => Method(writer, ...)". Changes span multiple Factories and Helpers files (13 files) and are purely stylistic/consistency updates with no behavioral change.
Change iidExpr to an IndentedTextWriterCallback and introduce a WriteInvokeBody callback to emit the delegate Invoke body via AbiMethodBodyFactory, reusing the interface CCW emitter. Reorder IID and Vtable accessors, move the UnmanagedCallersOnly Invoke method to use the new callbacks, and remove a duplicate MethodSignatureInfo by consolidating invoke parameter generation. These changes simplify codegen and avoid materializing IID as a string while enabling reuse of existing EmitDoAbiBodyIfSimple logic.
WinRT itself has no nullability annotations, so emitting '#nullable enable/disable'
directives, '?' on reference-type casts and returns, '[NotNullWhen]' attributes, and
the 'using System.Diagnostics.CodeAnalysis;' import into projection assemblies just
adds metadata for no runtime benefit.
Cleanup:
- Drop the 'isInNullableContext' parameter from 'UnsafeAccessorFactory.EmitIidAccessor'
and consolidate the split emission into a single multiline raw string ending in
'object _);'.
- Drop '#nullable enable' / '#nullable disable' directives previously emitted around
ABI marshaller classes for components, interfaces, and delegates.
- Drop '?' from emitted reference-type 'ConvertToManaged' return types and from the
matching casts in their bodies (classes, interfaces, delegates). The 'Nullable<T>'
annotation on '[WindowsRuntimeReferenceType(typeof(T?))]' for value-type enums
and structs is kept because '?' there means 'System.Nullable<T>', not a nullable
reference annotation.
- Drop '[NotNullWhen(true)]' and '?' from the emitted 'TryCreateObject' signature
('out object wrapperObject' is fully legal since the surrounding file is no longer
in '#nullable enable').
- Drop the now-unused 'using System.Diagnostics.CodeAnalysis;' from the standard
file header emitted by 'WriteFileHeader'.
Hand-authored files under 'Resources/Additions/' are NOT affected: those are
manually-written extension APIs (not auto-generated projections) and their
nullability annotations remain correct.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Delete the convenience overload of EmitIidAccessor(ProjectionEmitContext, GenericInstanceTypeSignature) that leased an IndentedTextWriter from IndentedTextWriterPool, emitted the IID accessor, and returned the resulting string. This simplifies the UnsafeAccessorFactory by removing the redundant helper method.
Refactor 'WriteComponentClassMarshaller' so the '[UnsafeAccessor]' declaration and the IID expression passed to 'WindowsRuntimeInterfaceMarshaller.ConvertToUnmanaged' are both produced by local 'IndentedTextWriterCallback'-compatible functions interpolated directly into the single multiline raw string template, rather than split across a conditional pre-write plus a separate template chunk. This reads top-to-bottom like the final emitted output and removes the artificial 'WriteLine(template1)' + 'EmitIidAccessor(...)' + 'WriteLine(template2)' split. Also opens the visibility of 'WriteComponentClassMarshaller', 'WriteAuthoringMetadataType', and 'BuildIidPropertyNameForGenericInterface' from 'internal' to 'public' so they can be referenced by consumers outside the writer assembly. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor 'WriteClassMarshallerStub' so the three branches of the 'ConvertToUnmanaged' body (sealed -> unwrap, unsealed-with-non-exclusive-iface -> 'IWindowsRuntimeInterface', fallback -> 'GetDefaultInterface') are emitted by a local 'IndentedTextWriterCallback' interpolated directly into the single multiline raw string template, rather than split across an opening 'WriteLine(template1)' + conditional 'WriteLine(branch)' chain + closing 'WriteLine(template2)'. This reads top-to-bottom like the final emitted output and keeps the 'Marshaller' class plus the immediately-following file-scoped 'ComWrappersMarshallerAttribute' declaration as one contiguous template. The local callback uses 'Write' (not 'WriteLine') so it does not add a trailing newline; the parent literal's leading '\n' after the placeholder supplies the newline. This pairing keeps the cursor at start-of-line for the branch's first emitted line (correct indentation) and avoids introducing a spurious blank line between the branch's closing '}' and 'return default;'. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor 'WriteDelegateEventSourceSubclass' so the entire 'EventSource<T>' class
body, including the 'EventState.GetEventInvoke()' return expression, is emitted
by a single multiline raw string template, rather than split across an opening
'Write(template1)' ending at 'return (' + two manual parameter-listing loops
interleaved with literal '") => TargetDelegate.Invoke("' + closing
'WriteLine(template2)'.
The two parameter lists are emitted via 'MethodFactory.WriteCallArguments(...)'
(an existing helper that returns an 'IndentedTextWriterCallback'), interpolated
twice into the template as '{{callArgs}}'. Both call sites are inside the same
return expression on the same line, matching the original output exactly.
No behavioral change: validation harness reports 0 diffs across all three
scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make 'IndentedTextWriter' automatically align newlines emitted by a callback
invoked from an '{{...}}' interpolation hole in a multiline raw string template
to the column the placeholder was at. This lets a callback emit raw multi-line
content (without baking the surrounding visual indent into its own literal)
and have it indent flush with the parent template, as if the content were
inlined at the placeholder's position.
Implementation:
- 'InvokeCallbackWithCursorIndent(object callback)' is the single dispatch
entry point. Typed as 'object' so the same path serves both
'IndentedTextWriterCallback' and 'Action<IndentedTextWriter>' shapes without
the caller allocating a closure to bridge them.
- Fast path: if the buffer is empty or ends with '\n' the callback is invoked
unchanged ('WriteRawText' already uses the writer's current indentation for
the first line and every subsequent newline).
- Slow path: scan back from the end of the buffer to the last '\n' to compute
the cursor column, divide by 'DefaultIndentation.Length' to get the
equivalent indent level, swap '_currentIndentation' with the cached string
for that level, invoke the callback, restore '_currentIndentation' in a
try/finally. Indentations only ever advance in multiples of
'DefaultIndentation' so the same level-indexed cache that 'IncreaseIndent'
uses can be reused, avoiding a parallel column-indexed array.
- A static local function inside the dispatch entry point pattern-matches the
'object' to the concrete delegate shape and invokes it directly, so the
generated IL is a couple of isinst / castclass checks instead of a closure
allocation.
A new private helper 'GetIndentationStringForLevel(int)' factors out the
"resize array and walk back to fill missing intermediates" logic. The cursor
column for a placeholder can correspond to a deeper level than any open
'Block' has reached, so the helper walks back to the highest populated entry
and builds the chain up from there. 'IncreaseIndent' is also rewritten to go
through this helper for symmetry.
The 'AppendFormatted<T>' overloads in both interpolated-string handlers route
'IndentedTextWriterCallback' and 'Action<IndentedTextWriter>' values through
'InvokeCallbackWithCursorIndent' directly (no lambda wrapper for actions).
Output impact (baselines updated in this commit): 373 occurrences across 95
files of method-body opening braces that were previously emitted at the
writer's base indent (one level less than the surrounding method signature,
due to the callback being placed mid-literal at a column the writer didn't
account for) now correctly align with the signature line. All affected lines
are pure whitespace shifts; there are no content changes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Now that cursor-aware indent in 'IndentedTextWriter' aligns newlines emitted
inside an interpolation-hole callback to the column the placeholder was at,
the 'WriteConvertToUnmanagedBranch' callback in 'WriteClassMarshallerStub'
can be placed visually inside the surrounding template at its natural
indentation level (rather than at the literal's outermost column with the
visual prefix baked into the callback's own raw string).
Changes:
- The '{{WriteConvertToUnmanagedBranch}}' placeholder now sits at the body
indent level inside the 'XMarshaller' template, matching the visual
position of the surrounding statements.
- The three branch literals inside the callback drop the previously
baked-in 8 spaces of visual indent and start at column 0 of their own
raw strings; the writer now adds the cursor-aligned indent at runtime.
- The 'defIfaceTypeName' computation in the middle branch is now an
'IndentedTextWriterCallback' interpolated directly rather than a
pre-materialized string, dropping one '.Format()' call.
Also surfaces an intentional one-line vertical separation between the
emitted branch body and the trailing 'return default;' statement, which is
new in the generated marshaller output.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…k emits nothing
Extend 'InvokeCallbackWithCursorIndent' so that, on the mid-line path, if the
callback returns without writing anything to the buffer AND the only content
between the last '\n' and the cursor at callback entry was whitespace (i.e.
the visual indent the parent literal wrote in front of an otherwise
line-start placeholder), the buffer is trimmed back to the position right
after the last '\n'. The buffer now ends with '\n', so the existing
blank-line suppression in 'AppendLiteral' fires on the next literal segment
and the empty callback collapses cleanly without leaving a stranded blank
line full of trailing whitespace.
This unblocks more split-template refactors: placeholders for optional
emissions (e.g. an attribute that only fires for generic instantiations)
can now be placed inside the body indent of a single multiline template
without producing a spurious blank line when the optional emission is
skipped.
The whitespace-only guard is essential: when a placeholder sits inline
after other content (e.g.
'public delegate {{ret}} {{name}}{{typeParams}}({{parms}});'), trimming
back to the last '\n' would strip the preceding signature, so the buffer
is left untouched in that case.
Validation harness: 0 diffs across all three scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor the three split-template emissions in 'WriteClassMarshallerStub'
to use the inline-callback pattern, leaning on the cursor-aware indent
recently added to 'IndentedTextWriter':
- Public '*Marshaller' class + file-scoped '*ComWrappersMarshallerAttribute'
are now one multiline template. The 'EmitUnsafeAccessorForDefaultIfaceIfGeneric'
call previously sandwiched between 'WriteBlock' open/close is now a
'{{WriteUnsafeAccessor}}' interpolation hole inline at the body indent.
- Sealed 'XComWrappersCallback' is now a single template with
'{{WriteUnsafeAccessor}}' interpolated inline.
- Unsealed 'XComWrappersCallback' (containing both 'TryCreateObject' and
'CreateObject') is also collapsed to a single template with the same
interpolation.
The new 'WriteUnsafeAccessor' local-function callback inlines the
'[UnsafeAccessor]' + 'static extern' emission with 'Write' (not
'WriteLine') so it doesn't leave a trailing newline; the parent literal's
own '\n' between the placeholder and the next member supplies the line
break. When the default interface is not a generic instantiation the
callback returns without writing anything, and the new whitespace-only
trim in 'InvokeCallbackWithCursorIndent' removes the stranded visual
indent so the empty placeholder collapses cleanly.
Output impact (baselines updated): 162 occurrences across 55 files of
'[UnsafeAccessor]' / 'static extern' declarations inside
'*ComWrappersCallback' classes (in the generic-default-interface case)
now correctly indent to the class body level rather than the writer base
level, fixing the same pre-existing indent quirk the cursor-aware change
fixed for delegate 'Invoke' braces. All affected lines are pure
whitespace shifts; no content changes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… into single templates
Refactor 'WriteInterfaceVftbl' and 'WriteInterfaceImpl' so the static
sections + per-method foreach assignments collapse into a single multiline
raw string template per method, leveraging the cursor-aware indent + empty
callback trim recently added to 'IndentedTextWriter':
- 'WriteInterfaceVftbl': merge the previously split 'WriteLine(struct decl)'
+ 'using (writer.WriteBlock()) { WriteLine(fixed entries); foreach (...) }'
pattern into one template with a '{{WriteMethodFields}}' callback. The
callback uses a first-flag + 'Write' (not 'WriteLine') so the last entry
doesn't end with '\n'; the literal's '\n}' continuation then closes the
struct on its own line without a stray blank line.
- 'WriteInterfaceImpl': similarly merge the static-constructor body, IID
property, and Vtable property emissions into a single 'Write' template.
'{{WriteVftblAssignments}}' is interpolated inside the static constructor
body to emit 'Vftbl.X = &Do_Abi_X;' lines via the same first-flag
pattern.
The 'using (writer.WriteBlock())' wrapping the class body in
'WriteInterfaceImpl' is kept because the class also contains the per-method
'Do_Abi_*' bodies emitted later by 'EmitOneDoAbi' (outside the merged
template).
No behavioral change: validation harness reports 0 diffs across all three
scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor 'WriteReferenceImpl' so the class declaration, Vtable static constructor, Vtable property, conditional 'get_Value' method body, and IID property are all emitted by a single multiline raw string template, rather than split across three 'WriteLine' / 'Write' chunks with a three-way conditional in the middle. The conditional 'get_Value' body (blittable struct / non-blittable struct / class-or-delegate dispatch, plus an unreachable fallback that throws) is hoisted into a 'WriteGetValueBody' local-function callback interpolated at the body indent of the merged template. Each branch's literal is now a plain multiline raw string without the previously baked-in 8 spaces of visual indent: the cursor-aware indent recently added to 'IndentedTextWriter' aligns subsequent lines to the placeholder's column at runtime. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…le templates Refactor 'WriteInterfaceIdicImplMembersForInterface' so each method and property emission collapses into one multiline raw string template per iteration, using local-function callbacks for the conditional parts. - Methods loop: the previous 'unsafe ' + 'Write(signature)' + 'WriteLine(body opening)' + 'WriteIf(return)' + 'Write(call)' + 'WriteLine(body close)' split is merged into a single 'WriteLine(template)' call. The conditional 'return' keyword is captured in a local 'returnKw' string and interpolated. The literal preserves the existing emission's per-line positions exactly (including the trailing-whitespace blank line and the 'return' line sitting at the class-body indent rather than the method-body indent, both pre-existing emission quirks). - Properties loop: the previous 'WriteLine(prop opening)' + conditional getter block + conditional synthetic getter + conditional setter block + 'WriteLine(close brace)' split is merged into one template with two 'WriteGetter' / 'WriteSetter' callbacks. Both callbacks use 'Write' (no trailing newline); when one of them is empty (e.g. getter-only properties), the empty-callback trim removes the stranded visual indent so the merged block emits cleanly with just the present accessor. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'UnsafeAccessorFactory.EmitStaticMethod' previously required callers to
include a leading ', ' separator in the 'parameterList' argument (so it
could be glued onto the trailing end of the synthetic 'object _' receiver
parameter), with the contract documented as "including the leading comma
when non-empty. Pass string.Empty for the parameter-less form."
Pre-pending the separator is now handled inside 'EmitStaticMethod': a
local 'commaPrefix' is set to ', ' when 'parameterList' is non-empty and
'' otherwise, and the template emits 'object _{{commaPrefix}}{{parameterList}}'.
Callers can now pass the actual parameter list verbatim, with no manual
separator boilerplate.
All 14 'EmitStaticMethod' call sites updated to strip the previously
required leading ', ' from their 'parameterList' argument:
- 'AbiMethodBodyFactory.DoAbi' (4 sites)
- 'AbiMethodBodyFactory.RcwCaller' (9 sites)
- 'ClassMembersFactory.WriteClassMembers' (2 sites)
- 'ClassMembersFactory.WriteInterfaceMembers' (1 site)
- 'ConstructorFactory.FactoryCallbacks' (3 sites)
- 'EventTableFactory' (1 site)
- 'MappedInterfaceStubFactory' (1 site)
The 'EmitConstructorReturningObject' callers, which already pass the
constructor's full parameter list without a leading comma, are unaffected.
No behavioral change: validation harness reports 0 diffs across all three
scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Factories codebase had 4 manual '[UnsafeAccessor(...)]' / 'static extern' emissions that bypassed 'UnsafeAccessorFactory.EmitStaticMethod', each producing buggy output: the attribute and its declaration were emitted in a single multiline template with the attribute at one visual indent and the declaration outdented (or further-indented) by 4 chars, breaking the visual alignment of the two lines. Centralized through 'EmitStaticMethod': - 'AbiMethodBodyFactory.RcwCaller' (Dispose accessor): the previous Write+WriteIf+Write pattern that conditionally appended ' data' to a non-blittable struct's param type (causing the param name to land on a separate line at a stray indent) is replaced by a single 'EmitStaticMethod' call. Both branches now include ' data' in 'disposeDataParamType' explicitly so the parameter list is well-formed. - 'AbiMethodBodyFactory.DoAbi' (CopyToManaged, ConvertToManaged, CopyToUnmanaged accessors): the previous 'WriteLine(multiline)' that emitted the attribute, the declaration, AND the immediate call site as three lines inside a single template is replaced by an 'EmitStaticMethod' call for the declaration followed by a plain 'writer.WriteLine(...)' for the call. The unused 'using System;' import in 'RcwCaller.cs' (the 'StringComparison.Ordinal' call that referenced it has been removed) is also dropped. Output impact (baselines updated): 35 files with pure whitespace shifts: the affected 'static extern' declarations and their call lines now correctly align with their '[UnsafeAccessor]' attribute (rather than being over-indented by 4 chars), and the non-blittable Dispose declaration no longer breaks across two lines at the parameter name. No content changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…EmitIidAccessor The 'WriteUnsafeAccessor' local callback in 'AbiClassFactory.WriteClassMarshallerStub' was the last manual '[UnsafeAccessor]' + 'static extern' emission in the Factories codebase that bypassed the centralized 'UnsafeAccessorFactory'. It inlined an exact copy of 'EmitIidAccessor's body except using 'Write' (no trailing newline) instead of 'WriteLine', because it runs inside an interpolation-hole callback where the parent literal's '\n' supplies the line break. 'EmitIidAccessor' now takes an optional 'appendNewline = true' parameter: - 'true' (default) preserves the existing behavior — the writer's cursor advances to the start of the next line after the declaration (suitable for standalone emission followed by another 'WriteLine'). - 'false' uses 'Write' instead of 'WriteLine' — the cursor stays at the end of the closing ';' so the calling interpolation-hole template owns the line break. 'AbiClassFactory.WriteClassMarshallerStub' now delegates its 'WriteUnsafeAccessor' local callback to 'EmitIidAccessor(... appendNewline: false)'. All remaining matches for '[UnsafeAccessor' / 'UnsafeAccessorType' / 'static extern' across the codebase fall into one of three buckets: XML-doc / comment references, hand-authored 'Resources/Base/ComInteropExtensions.cs' helpers (out of scope per established rule for hand-authored resource files), or the 'UnsafeAccessorFactory' itself. No stray manual emissions remain in the dynamic code-generation paths. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ading newline
Add a second case to 'AppendLiteral's blank-line-suppression rule so that a
literal segment whose leading newline immediately follows a callback that
ended with '\n' (e.g. a callback that dispatches to 'WriteLine') doesn't
emit a stray blank line at the seam.
The rule now fires for either of:
(a) the previous interpolation hole emitted NOTHING and the buffer
already ends with '\n' (the existing empty-callback case), OR
(b) the previous interpolation hole DID emit content AND that content
ended with '\n', so a literal '\n' immediately after would be a
second consecutive newline.
Tracking is done via a new 'AppendInterpolatedStringHandler._lastInterpolationEndedWithNewline'
flag that mirrors 'AppendFormatted's existing '_anyContentBetweenLiterals'
update: when a callback grows the buffer and the buffer then ends with '\n',
the flag is set; both flags reset on every 'AppendLiteral'.
This unblocks callers that want to centralize emission through helpers like
'UnsafeAccessorFactory.EmitIidAccessor' (which use 'WriteLine' and thus add
a trailing '\n') from cursor-aware interpolation-hole callbacks where the
parent template's continuation also starts with '\n'.
As an incidental fix, 'AppendFormatted's 'Action<IndentedTextWriter>'
branch no longer early-returns: it now falls through to the tracking-flag
update so the empty-callback trim and the new blank-line rule both
correctly cover that delegate shape as well.
No behavioral change for existing callers: validation harness reports 0 diffs
across all three scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ssor Now that the writer's blank-line suppression rule (commit 18532af) also collapses the seam between a callback's trailing '\n' and a literal's leading '\n', 'EmitIidAccessor' can unconditionally use 'WriteLine' just like the other 'UnsafeAccessorFactory' methods. 'AbiClassFactory.WriteClassMarshallerStub's 'WriteUnsafeAccessor' callback calls 'EmitIidAccessor(writer, context, gi)' directly; the parent template's '\n' before the next member no longer produces a stray blank line because the writer collapses it with the accessor's trailing '\n'. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…D accessor callback
The 'WriteIidAccessor' local callback in 'WriteComponentClassMarshaller'
emitted the '[UnsafeAccessor]' declaration via 'EmitIidAccessor' (which
itself uses 'WriteLine' and so leaves the buffer ending with '\n') and
then explicitly called 'writer.WriteLine()' to advance past the next
line. The parent template's literal continuation between the
'{{WriteIidAccessor}}' placeholder and the next line already starts with
'\n'; the extra 'WriteLine()' was inserted to compensate for a
not-yet-existing blank-line-suppression rule.
Commit 18532af added that rule: when a callback's content ends with '\n'
AND the next literal segment starts with '\n', the literal's leading '\n'
is dropped to collapse the double newline. So the explicit 'WriteLine()'
inside the callback is now redundant — without it the parent literal's
'\n' supplies the line break, the callback's own '\n' is preserved, and
no stray blank line is emitted.
No behavioral change: validation harness reports 0 diffs across all three
scenarios.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor the composable-ctor emission in 'ConstructorFactory.Composable' so the base-initializer close, body opening, conditional default-iface assign, and optional GC.AddMemoryPressure call collapse into a single multiline template. The body is emitted by a 'WriteCtorBody' local callback interpolated at the body indent of the merged template. The 'using (writer.WriteBlock())' wrapper previously bracketed the body and produced a method-body indent that didn't match the rest of the ctor emission, leaving the optional GC.AddMemoryPressure line at the writer base indent rather than the body indent (a pre-existing quirk preserved by the callback's own indentation). No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor the RCW-style constructor emission in 'ClassFactory.WriteRuntimeClass' so the signature, base initializer, body opening, conditional default-iface assign for unsealed classes, and optional GC.AddMemoryPressure call all collapse into a single multiline raw string template. The body is emitted by a 'WriteCtorBody' local callback interpolated at the body indent of the merged template. Previously: 'WriteLine(sig + base)' + 'using (writer.WriteBlock())' + nested conditional emissions for the two body lines. The 'using/WriteBlock' wrapper is gone; the merged template owns the brace structure. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…o single template Refactor 'EmitFactoryCallbackClass' so the private callback class declaration, the static Instance field, the '[MethodImpl]' attribute, the multi-line Invoke override signature, and the opening body brace collapse into a single multiline raw string template. The conditional Invoke signature (composable variant adds 'baseInterface' input + 'innerInterface' out parameters that the sealed variant doesn't have) is emitted by a 'WriteInvokeSignature' local callback interpolated into the template. Previously: 'WriteLine(class header)' + 'IncreaseIndent' + conditional 'WriteLine(invoke signature with opening brace)' + ref-mode early return or non-ref body. The 'IncreaseIndent' that wrapped the previous Invoke signature emission is gone (the merged template emits at the right indent directly); the second 'IncreaseIndent' that wrapped the body remains. The ref-mode early-return path now skips the extra 'DecreaseIndent' call that previously balanced the now-removed 'IncreaseIndent'. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…o single template Refactor the 'ComWrappersMarshallerAttribute' emission in 'WriteStructEnumMarshallerClass' so the class declaration, the three override methods (GetOrCreateComInterfaceForObject, ComputeVtables, CreateObject), and the conditional CreateObject body collapse into a single multiline raw string template. The body branch (complex struct round-trips through 'XMarshaller.ConvertToManaged'; enum / blittable unboxes directly) is emitted by a 'WriteCreateObjectBody' local callback interpolated at the body indent. Previously: 'WriteLine(class + 3 method headers + first stmt)' + 'IncreaseIndent' x 2 + conditional 'WriteLine(return ...)' + 'DecreaseIndent' / 'WriteLine(\"}\")' x 2. The 'IncreaseIndent' / 'DecreaseIndent' bookkeeping is gone; the merged template owns the brace structure. The 'WriteAbiStructOrEnumImpl' outer 'Marshaller' class (with its per-field foreach loops over ConvertToUnmanaged / ConvertToManaged / Dispose) is left unchanged: each loop emits a comma-separated list of object-initializer expressions where the surrounding scaffolding (IncreaseIndent / DecreaseIndent to bracket the field list indentation) is tightly coupled to the per-field emission. Inlining that would require restructuring the field loops themselves, which is a much larger change with higher risk; defer. No behavioral change: validation harness reports 0 diffs across all three scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #2415 (initial C++→C# port of
cswinrt) and #2418 (first refactor pass). This PR adds a much larger refactor pass on the C# projection writer ‒ infrastructure for interpolated-string writing and callback-based code emission, several new resolver/classifier types, broad helper consolidation across the writer, removal of dead code identified by multi-agent fleet analysis, and elimination of legacy projection modes that no longer apply.All scenario diffs remain at zero across
pushnot,everything-with-ui, andwindows: nothing here changes the generated projection output.Motivation
The initial C++→C# port (#2415) and the first refactor pass (#2418) gave us a working, faithful C# projection writer. With that baseline in place, this PR focuses on internal API quality so the writer is easier to read, maintain, and extend:
string.Format/WriteLineboilerplate that hid intent and made multi-line code awkward to read. AnIndentedTextWriterinterpolated-string handler plus anIIndentedTextWriterCallbackcallback model collapses dozens of multi-step writes into single interpolated raw-string templates.AbiTypeHelpers. Two new resolvers (TypeKindResolver, the renamedAbiTypeKindResolver) and several new extension properties onTypeDefinitiongive the writer a single, semantic entry point for "what kind of type is this?".MethodSignatureInfo,ParameterCategory, attribute lookup, and identifier-escaping logic were duplicated across factories. New extension/helper types (MethodSignatureMarshallingFacts,MethodSignatureInfoExtensions,ParameterInfoExtensions,IHasCustomAttributeExtensions,IdentifierEscaping,BuildGlobalQualifiedName, …) consolidate those into one shared, documented surface.TypeCategoryvsTypeKind). All of that is now cleaned up.CsWinRTEmbedded*and theProjectionInternalprivate-projection support) had no callers in CsWinRT 3.0 and have been deleted end-to-end.Changes
Grouped thematically (many small commits in each group):
IndentedTextWriter + callback emission model
Writers/IndentedTextWriter.AppendInterpolatedStringHandler.csandWriters/IndentedTextWriter.AppendIfInterpolatedStringHandler.cs: interpolated-string handlers forWrite/WriteLine/WriteIf/WriteLineIf, with the canonicalisMultiline: true, $""""""…""""""argument order at all call sites.Writers/IIndentedTextWriterCallback.cs+IIndentedTextWriterCallbackExtensions.cs: callback infrastructure that lets multi-line templates host arbitrary writer-driven holes, eliminating manualWriteinterleaving.Factories/*Callbacks/*Callback.cs: a family of named callbacks (WriteTypeNameCallback,WriteParameterListCallback,WriteIidExpressionCallback,WriteEventTypeCallback,WriteAbiTypeCallback,WriteFactoryMethodParametersCallback,WriteGuidAttributeCallback, etc.) extracted from inline-string emission and applied across the factories.New resolvers and classifiers
Resolvers/AbiTypeKindResolver.cs(renamed fromAbiTypeShapeResolver;AbiTypeShapewrapper deleted; classification logic inlined intoResolve).Resolvers/TypeKindResolver.cs: single static entry point classifying aTypeDefinitionasTypeKind.Class/Interface/Enum/Struct/Delegate(replacesTypeCategorization.GetCategory).Resolvers/ParameterCategoryResolver.cs:GetParamCategoryrenamed toResolvefor consistency.Models/TypeKind.cs,Models/AbiArrayElementKind.cs,Models/AbiTypeKind.cs,Models/MethodSignatureMarshallingFacts.cs,Models/MethodSignatureInfoExtensions.cs,Models/ParameterInfoExtensions.cs: new model types and extension helpers.Extension/helper consolidation
Extensions/TypeDefinitionExtensions.cs: new classifier properties (IsStruct,IsAttributeType,IsStatic,IsFlagsEnum,IsGeneric) and WinRT-specific predicates (IsApiContractType,IsExclusiveTo,IsProjectionInternal);Metadata/TypeCategorization.csdeleted.Extensions/TypeSignatureExtensions.cs,ITypeDescriptorExtensions.cs,IHasCustomAttributeExtensions.cs,ITypeDefOrRefExtensions.cs,InterfaceImplementationExtensions.cs,MethodDefinitionExtensions.cs,EventDefinitionExtensions.cs,PropertyDefinitionExtensions.cs,CustomAttributeExtensions.cs,FieldDefinitionExtensions.cs: many small extension methods (e.g.Names(),GetRawName(),GetRawNamespace(),MatchesName,TryResolveTypeDef,TryGetFixedArgument<T>,HasNonObjectBaseType,IsAbiRefLike,IsArrayInput,IsReferenceTypeOrGenericInstance,AsSzArray,GetStrippedName,StripByRefAndCustomModifiers,BuildGlobalQualifiedName, …).Helpers/AbiTypeHelpers.*.cs: split into focused partials;IsAnyStructrenamed toIsBlittableStruct;ComplexStructrenamed toNonBlittableStruct; misleading "almost-blittable" comments eliminated;GetAbiLocalTypeName,InterfacesEqualByName,GetNullableInnerInfo,IsBlittableAbiElementconsolidated into shared helpers.Helpers/IdentifierEscaping.cs+ParameterInfo.GetEscapedName: single source of truth for C# identifier escaping.Helpers/SignatureGenerator.cs: extracted from inline signature-string construction across factories; emits via writer to avoid intermediate string allocations.Factory clean-ups and consolidations
Factories/StructEnumMarshallerFactory.cs:Box/Unbox/CreateObject/complex-struct emission paths rewritten to use the callback infrastructure; struct-classifier terminology updated;GetInstanceFieldsextracted.Factories/ComponentFactory.cs: factory-class emission consolidated via a singleactivateBodylocal; reference-projection events & static factory support.Factories/AbiClassFactory.cs,Factories/AbiInterfaceFactory.cs,Factories/AbiDelegateFactory.cs,Factories/AbiStructFactory.cs,Factories/ClassFactory.cs,Factories/ConstructorFactory.cs,Factories/EventTableFactory.cs,Factories/InterfaceFactory.cs,Factories/MetadataAttributeFactory.cs,Factories/ReferenceImplFactory.cs: collapsed branch dispatches, unified default/exclusive-to interface emission, deduplicated property and event-accessor patterns, removed dead 4-branch chains, replaced ad-hoc generic-instance flow with helper-driven code paths.Dead-code and legacy cleanup
ArrayParametersandHasParameterOfCategory(unused).ClassMembersFactory.ResolveInterface(redundant) andIndentedTextWriterExtensions.cs(superseded by the interpolated-string handler)..winmdinputs (e.g.System.Exceptionchecks in classifiers that only ever seesWindows.Foundation.HResult).CsWinRTEmbedded*and embedded-projection-mode leftovers.ProjectionInternalmode is no longer a CsWinRT 3.0 feature).Bug fixes
HasNonObjectBaseTypewas incorrectly returningtruefor theSystem.Objectbase case — fixed to correctly recognise it.BuildGlobalQualifiedNamenow strips backticks (IList\1→IList) so callers don't have to pre-strip.Misc
ProjectionWriter.cs/ProjectionWriterOptions.cs/cswinrt.slnx: small surface trims tied to the removed legacy modes.