Skip to content

[net11.0][XSG] Trimmable Styles#33561

Open
simonrozsival wants to merge 11 commits intonet11.0from
dev/simonrozsival/trimmable-styles
Open

[net11.0][XSG] Trimmable Styles#33561
simonrozsival wants to merge 11 commits intonet11.0from
dev/simonrozsival/trimmable-styles

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented Jan 16, 2026

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description

Implements lazy/trimmable styles for XAML Source Generation, enabling the IL trimmer to remove unused styles and their target types from compiled apps.

Fixes #33156

How It Works

New Style Constructor and LazyInitialization Property

// New constructor accepts assembly-qualified type name (trim-safe)
public Style(string assemblyQualifiedTargetTypeName) { ... }

// Initializer delegate called lazily on first application  
public Action<Style, BindableObject> LazyInitialization { private get; set; }

Generated Code Pattern

The source generator now emits:

var style = new Style("Microsoft.Maui.Controls.Label, Microsoft.Maui.Controls");
style.LazyInitialization = (__style, __target) =>
{
    // Type guard - skips if Label was trimmed away
    if (__target is not global::Microsoft.Maui.Controls.Label) return;
    
    // Setters populated here
    var setter = new Setter { Property = Label.TextColorProperty, Value = Colors.Red };
    __style.Setters.Add(setter);
};

Key Implementation Details

  • IStyle.TargetType returns Type? (nullable) - returns null if type was trimmed
  • Style.ResolveTargetType() uses [UnconditionalSuppressMessage] to suppress IL2057 - intentionally allows types to be trimmed
  • Style.InitializeIfNeeded(target) called on first IStyle.Apply() - runs the initializer once with proper locking
  • Style.TargetTypeFullName provides trim-safe type comparison without resolving the Type

Changes Summary

Area Key Changes
Style.cs New constructor, LazyInitialization property, InitializeIfNeeded(), ResolveTargetType()
IStyle.cs TargetType is now nullable (Type?)
SourceGen All visitors updated with StopOnStyle/VisitNodeOnStyle/IsStyle for special Style handling
XamlC Same visitor interface changes (XamlC does NOT use lazy styles)
Tests Comprehensive unit tests for lazy style behavior

What This Enables

  1. Trimmer can remove unused styles - If a style's target type is trimmed, TargetType returns null and the style is skipped at runtime
  2. Deferred initialization - Setters/Behaviors/Triggers are only created when the style is first applied
  3. Type guards - Generated code includes if (__target is not TargetType) return; to handle trimmed types gracefully

TODO

  • Verify trimming actually works end-to-end with a trimmed publish
  • Full UI test validation
  • Consider dedicated UI test for implicit style BasedOn/ApplyToDerivedTypes behavior

@simonrozsival simonrozsival changed the title [WIP] Trimmable/Lazy Styles for SourceGen [WIP][XSG] Trimmable/Lazy Styles for SourceGen Jan 16, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-styles branch from bfcfe86 to 01cf3de Compare January 16, 2026 13:21
@simonrozsival simonrozsival changed the title [WIP][XSG] Trimmable/Lazy Styles for SourceGen [WIP][net11.0][XSG] Trimmable/Lazy Styles for SourceGen Jan 16, 2026
@simonrozsival simonrozsival changed the base branch from main to net11.0 January 16, 2026 13:22
@simonrozsival simonrozsival added xsg Xaml sourceGen perf/app-size Application Size / Trimming (sub: perf) labels Jan 16, 2026
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-styles branch 6 times, most recently from 64a71d2 to eab10eb Compare January 16, 2026 14:48
@simonrozsival simonrozsival changed the title [WIP][net11.0][XSG] Trimmable/Lazy Styles for SourceGen [net11.0][XSG] Trimmable Styles Jan 20, 2026
@simonrozsival simonrozsival marked this pull request as ready for review January 20, 2026 12:47
Copilot AI review requested due to automatic review settings January 20, 2026 12:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements lazy/trimmable styles for XAML Source Generation, enabling the IL trimmer to remove unused styles and their target types from compiled apps. The feature addresses issue #33156 by deferring style initialization until first application and using string-based type names instead of Type objects for AOT compatibility.

Changes:

  • Added new Style(string assemblyQualifiedTargetTypeName) constructor and LazyInitialization property for deferred initialization
  • Modified IStyle.TargetType to be nullable (Type?) to support trimmed types
  • Updated source generator to emit lazy style initialization code with type guards
  • Extended visitor infrastructure with StopOnStyle, VisitNodeOnStyle, and IsStyle methods across all visitors (both SourceGen and XamlC)

Reviewed changes

Copilot reviewed 47 out of 47 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Style.cs Adds lazy constructor, LazyInitialization property, nullable TargetType resolution, and TargetTypeFullName for trim-safe type comparison
IStyle.cs Changes TargetType to nullable Type? to support trimmed types
MergedStyle.cs Updates to handle nullable TargetType from IStyle
ResourceDictionary.cs Uses TargetTypeFullName instead of TargetType.FullName for adding styles
CreateValuesVisitor.cs Implements TryCreateTrimmableStyle to generate string-based Style constructor
SetPropertiesVisitor.cs Generates LazyInitialization lambda with type guard before style property assignment
SetNamescopesAndRegisterNamesVisitor.cs Adds null checks for nodes not in Variables dictionary (Style content)
XamlNode.cs Implements IsStyleContent logic for skipping Style children during normal traversal
XmlName.cs Adds _StyleContent marker for identifying styles needing lazy initialization
All visitor files (20+) Implements StopOnStyle, VisitNodeOnStyle, and IsStyle interface methods
PublicAPI files (7) Adds new public API entries for Style constructor and LazyInitialization setter
Test files (10+) Updates tests to call InitializeIfNeeded for SourceGen inflator, adds comprehensive lazy style tests

return false;
do
{
targetType = targetType.BaseType;
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The CanBeAppliedTo method has a potential null reference exception. When checking ApplyToDerivedTypes, the code iterates through the base types and calls targetType.FullName without checking if targetType is null. If targetType.BaseType returns null (which happens when reaching the top of the type hierarchy), the code will throw a NullReferenceException on the next iteration when accessing targetType.FullName.

Add a null check before accessing targetType.FullName in the loop.

Suggested change
targetType = targetType.BaseType;
targetType = targetType.BaseType;
if (targetType == null)
return false;

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +168
return _targetType.FullName.AsSpan();

// Extract FullName from AQN: "Namespace.TypeName, AssemblyName, ..."
// FullName is everything before the first comma
Debug.Assert(_assemblyQualifiedTargetTypeName is not null, "Either _targetType or _assemblyQualifiedTargetTypeName must be set");
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The TargetTypeFullName property may throw a NullReferenceException when _targetType is not null but _targetType.FullName is null. For generic types or special type scenarios, Type.FullName can return null according to the .NET documentation. This would cause AsSpan() to throw.

Add a null check for _targetType.FullName before calling AsSpan(), or handle the null case by falling back to the assembly-qualified name extraction logic.

Suggested change
return _targetType.FullName.AsSpan();
// Extract FullName from AQN: "Namespace.TypeName, AssemblyName, ..."
// FullName is everything before the first comma
Debug.Assert(_assemblyQualifiedTargetTypeName is not null, "Either _targetType or _assemblyQualifiedTargetTypeName must be set");
{
var fullName = _targetType.FullName;
if (fullName is not null)
return fullName.AsSpan();
// If FullName is null (e.g., for certain generic or special types),
// fall back to the assembly-qualified name parsing below.
}
// Extract FullName from AQN: "Namespace.TypeName, AssemblyName, ..."
// FullName is everything before the first comma
Debug.Assert(_assemblyQualifiedTargetTypeName is not null, "When _targetType.FullName is null, _assemblyQualifiedTargetTypeName must be set");

Copilot uses AI. Check for mistakes.
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-styles branch from e7d801f to d0c0ce4 Compare January 20, 2026 13:31
@rmarinho
Copy link
Copy Markdown
Member

/rebase

@simonrozsival
Copy link
Copy Markdown
Member Author

@copilot this PR needs a proper rebase with conflict resolution. open a PR which addresses this issue. make sure it is up to date with the base net11.0 branch.

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 12, 2026

@simonrozsival I've opened a new pull request, #34011, to work on those changes. Once the pull request is ready, I'll request review from you.

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-styles branch from 3536086 to fefc63a Compare February 13, 2026 08:22
simonrozsival added a commit that referenced this pull request Feb 18, 2026
…hanges

These changes belong in separate PRs:
- #33611 (CSS StyleSheet trimming)
- #33561 (EventTrigger trimming)
- #33160 (HybridWebView trimming)

Reverted files to their main branch state, keeping only the
[ElementHandler] attribute addition on HybridWebView.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@StephaneDelcroix StephaneDelcroix force-pushed the dev/simonrozsival/trimmable-styles branch from fefc63a to 59a9982 Compare February 27, 2026 15:27
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 27, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33561

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33561"

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-styles branch from 59a9982 to 9fb44d8 Compare March 11, 2026 10:00
@simonrozsival
Copy link
Copy Markdown
Member Author

Self-review notes

1. Extract helper for repeated ((IStyle)style).Apply(...) pattern

The pattern ((IStyle)style).Apply(new BoxView(), new SetterSpecificity()) appears 6+ times in the style tests. Add a helper method to reduce boilerplate:

static void ForceApplyLazyStyle(Style style, BindableObject target)
{
    ((IStyle)style).Apply(target, new SetterSpecificity());
}

2. Revert unnecessary whitespace changes

These files have whitespace-only diffs (extra blank line added) unrelated to this PR:

  • src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SetterCompiledConverters.cs
  • src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SimplifyOnPlatform.cs
  • src/Controls/tests/SourceGen.UnitTests/Maui32879Tests.cs

3. Duplicate ShellFlyoutRenderer entry in PublicAPI.Unshipped.txt

The net-ios and net-maccatalyst PublicAPI.Unshipped.txt files already contain:

~override Microsoft.Maui.Controls.Platform.Compatibility.ShellFlyoutRenderer.ViewWillTransitionToSize(...) -> void

This PR duplicates that line. Remove the duplicate — it is unrelated to the style changes.

@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-styles branch from 29da551 to a14c299 Compare March 20, 2026 09:08
@simonrozsival
Copy link
Copy Markdown
Member Author

@StephaneDelcroix @rmarinho could you please have a look at this PR and let me know what you think about it? could we merge this into net11.0 soon?

simonrozsival and others added 9 commits March 31, 2026 19:32
Squashed for clean rebase.
- Inline the lazy initialization lock+check directly into IStyle.Apply
- Remove the internal InitializeIfNeeded method (was only needed for tests)
- Update tests to use ((IStyle)style).Apply() instead of InitializeIfNeeded

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Duplicate was introduced during rebase conflict resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- CanBeAppliedTo: add null check after BaseType walk (reaches null at
  top of hierarchy before hitting Element)
- TargetTypeFullName: guard against Type.FullName being null for
  generic/special types; fall through to AQN parsing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- PublicAPI net-ios/net-maccatalyst: remove duplicate ShellFlyoutRenderer entry
- Revert whitespace-only changes in 3 SourceGen test snapshot files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace 6 occurrences of ((IStyle)style).Apply(target, new SetterSpecificity())
with style.ForceInitialize(target) extension method.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SetPropertiesVisitor: remove duplicate variable declarations and
  duplicate getNodeValue delegate from rebase conflict
- SetNamescopesAndRegisterNames: remove duplicate if-statement, keep
  TryGetValue guard for nodes not in Variables
- Style.CanBeAppliedTo: guard against null Type.FullName

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Change TargetTypeFullName from ReadOnlySpan<char> to string, removing
  #if NETSTANDARD branching and Span-based comparisons
- Use plain string equality in CanBeAppliedTo instead of SequenceEqual
- Remove redundant .ToString() call in ResourceDictionary.Add
- Revert unrelated maps PublicAPI.Unshipped.txt changes from rebase

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename parentVar in nested scope to avoid CS0136.
Add StringComparison.Ordinal to IndexOf call to satisfy CA1307.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/trimmable-styles branch from fe637c1 to 2b30cbd Compare March 31, 2026 17:34
- SetPropertiesVisitor.cs: Use Context property (not primary ctor param)
  since this class uses a regular constructor
- Style.cs: Remove StringComparison.Ordinal from char IndexOf overload
  for netstandard2.0 compatibility

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
if (!ApplyToDerivedTypes)
return false;
do
while (targetType != typeof(Element))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 The do-whilewhile refactor here changed semantics. If targetType is exactly typeof(Element) and ApplyToDerivedTypes=true, the old code entered the loop body first (getting BaseType), but the new while loop checks the condition first — so it never enters.

Old behavior:

do {
    targetType = targetType.BaseType;       // Element → NavigableElement
    if (TargetType == targetType) return true; // match!
} while (targetType != typeof(Element));

New behavior:

while (targetType != typeof(Element))  // Element == Element → false, skip loop
{
    // never reached
}
return false;  // ← regression

Suggested fix — restore do-while with the null guard:

do
{
    targetType = targetType.BaseType;
    if (targetType is null)
        return false;
    if (targetType.FullName is not null && TargetTypeFullName == targetType.FullName)
        return true;
} while (targetType != typeof(Element));
return false;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf/app-size Application Size / Trimming (sub: perf) xsg Xaml sourceGen

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[XSG] Generate trimmable styles

5 participants