Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Controls/src/SourceGen/NodeSGExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,15 @@ public static bool TryProvideValue(this ElementNode node, IndentedTextWriter wri
if (GetKnownValueProviders(context).TryGetValue(variable.Type, out var valueProvider)
&& valueProvider.TryProvideValue(node, writer, context, getNodeValue, out returnType0, out value))
{
// Check for "skip this node" sentinel: returnType is null and value is empty
// This happens when a Setter has no value (e.g., OnPlatform with no matching platform)
if (returnType0 is null && string.IsNullOrEmpty(value))
{
// Remove from Variables so it won't be added to any collection
context.Variables.Remove(node);
return true;
}

var variableName = NamingHelpers.CreateUniqueVariableName(context, returnType0 ?? context.Compilation.ObjectType);
context.Writer.WriteLine($"var {variableName} = {value};");
context.Variables[node] = new LocalVariable(returnType0 ?? context.Compilation.ObjectType, variableName);
Expand Down
4 changes: 4 additions & 0 deletions src/Controls/src/SourceGen/SetPropertyHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public static void SetPropertyValue(IndentedTextWriter writer, ILocalValue paren
return;
}

// If the node was removed from Variables (e.g., Setter with no value due to OnPlatform), skip silently
if (valueNode is ElementNode en && !context.Variables.ContainsKey(en))
return;

var location = LocationCreate(context.ProjectItem.RelativePath!, (IXmlLineInfo)valueNode, localName);
context.ReportDiagnostic(Diagnostic.Create(Descriptors.MemberResolution, location, localName));
}
Expand Down
10 changes: 9 additions & 1 deletion src/Controls/src/SourceGen/SetterValueProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public bool CanProvideValue(ElementNode node, SourceGenContext context)
// Get the value node (shared logic with TryProvideValue)
var valueNode = GetValueNode(node);

// Must have a value node to provide a value
// This can be null when OnPlatform removes the Value property (no matching platform, no Default)
if (valueNode == null)
return false;

// Value must be a simple ValueNode (not a MarkupNode or ElementNode)
if (valueNode is MarkupNode or ElementNode)
return false;
Expand All @@ -38,8 +43,11 @@ public bool TryProvideValue(ElementNode node, IndentedTextWriter writer, SourceG
var valueNode = GetValueNode(node);
if (valueNode == null)
{
// The value was removed (e.g., OnPlatform with no matching platform and no Default)
// Signal to skip this Setter entirely by returning true with null returnType and empty value
returnType = null;
value = string.Empty;
return false;
return true;
}

var bpNode = (ValueNode)node.Properties[new XmlName("", "Property")];
Expand Down
9 changes: 6 additions & 3 deletions src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@ public void Visit(ElementNode node, INode parentNode)
}
else if (parentVar.Type.CanAdd(context))
{
Writer.WriteLine($"{parentVar.ValueAccessor}.Add({Context.Variables[node].ValueAccessor});");
// Skip if the node was removed from Variables (e.g., Setter with no value due to OnPlatform)
if (Context.Variables.TryGetValue(node, out var nodeVar))
Writer.WriteLine($"{parentVar.ValueAccessor}.Add({nodeVar.ValueAccessor});");
}
else
{
Expand Down Expand Up @@ -196,8 +198,9 @@ public void Visit(ElementNode node, INode parentNode)

if (propertyType.CanAdd(context))
{
Writer.WriteLine($"{variable.ValueAccessor}.Add({Context.Variables[node].ValueAccessor});");

// Skip if the node was removed from Variables (e.g., Setter with no value due to OnPlatform)
if (Context.Variables.TryGetValue(node, out var nodeVar))
Writer.WriteLine($"{variable.ValueAccessor}.Add({nodeVar.ValueAccessor});");
}
else
//report diagnostic: not a collection
Expand Down
13 changes: 13 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui33676.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui33676">
<ContentPage.Resources>
<!-- This should work when building for iOS -->
<Style TargetType="Label" x:Key="iOSOnlyStyle">
<Setter Property="Margin" Value="{OnPlatform iOS='0, 4, 0, 0'}" />
</Style>
<!-- This should also work when building for Android (no matching platform, no Default) -->
</ContentPage.Resources>
<Label x:Name="label" Style="{StaticResource iOSOnlyStyle}"/>
</ContentPage>
96 changes: 96 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui33676.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using Microsoft.Maui.Controls.Core.UnitTests;
using Microsoft.Maui.Devices;
using Xunit;

using static Microsoft.Maui.Controls.Xaml.UnitTests.MockSourceGenerator;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui33676 : ContentPage
{
public Maui33676() => InitializeComponent();

[Collection("Issue")]
public class Tests : IDisposable
{
MockDeviceInfo mockDeviceInfo;

public Tests() => DeviceInfo.SetCurrent(mockDeviceInfo = new MockDeviceInfo());

public void Dispose() => DeviceInfo.SetCurrent(null);

[Theory]
[XamlInflatorData]
// BUG: When a Setter uses OnPlatform without a Default value and the target platform
// doesn't match any of the specified platforms, SourceGen generates invalid code:
// ((IValueProvider)).ProvideValue(...) - trying to call a method on a type instead of an instance
//
// Expected behavior: The Setter should be skipped entirely or use a default value
internal void SetterWithOnPlatformWithoutDefaultShouldNotGenerateInvalidCode(XamlInflator inflator)
{
// Test compiling for Android when OnPlatform only specifies iOS
if (inflator == XamlInflator.SourceGen)
{
var result = CreateMauiCompilation()
.WithAdditionalSource(
"""
namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui33676 : ContentPage
{
public Maui33676() => InitializeComponent();
}
""")
.RunMauiSourceGenerator(typeof(Maui33676), targetFramework: "net10.0-android");

var generated = result.GeneratedInitializeComponent();

// BUG: Generated code contains invalid C# like:
// ((global::Microsoft.Maui.Controls.Xaml.IValueProvider)).ProvideValue(...)
// This causes: CS0119 'IValueProvider' is a type, which is not valid in the given context
// This should NOT happen - when OnPlatform has no matching value, the Setter should be omitted
Assert.DoesNotContain("((global::Microsoft.Maui.Controls.Xaml.IValueProvider)).ProvideValue", generated, StringComparison.Ordinal);

Assert.Empty(result.Diagnostics);
}
else
{
mockDeviceInfo.Platform = DevicePlatform.Android;
var page = new Maui33676(inflator);
Assert.NotNull(page);
}
}

[Theory]
[XamlInflatorData]
// When the platform DOES match, the Setter should work correctly
internal void SetterWithOnPlatformMatchingPlatform(XamlInflator inflator)
{
if (inflator == XamlInflator.SourceGen)
{
var result = CreateMauiCompilation()
.WithAdditionalSource(
"""
namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui33676 : ContentPage
{
public Maui33676() => InitializeComponent();
}
""")
.RunMauiSourceGenerator(typeof(Maui33676), targetFramework: "net10.0-ios");

Assert.Empty(result.Diagnostics);
var generated = result.GeneratedInitializeComponent();
Assert.Contains("0, 4, 0, 0", generated, StringComparison.Ordinal);
}
else
{
mockDeviceInfo.Platform = DevicePlatform.iOS;
var page = new Maui33676(inflator);
Assert.NotNull(page);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui33676_VisualState">
<ContentPage.Resources>
<Color x:Key="Gray950">#0D0D0D</Color>
<Color x:Key="Gray200">#E0E0E0</Color>
<Style TargetType="Button" x:Key="ButtonWithVisualState">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{OnPlatform Android='Red'}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ContentPage.Resources>
<Button x:Name="button" Style="{StaticResource ButtonWithVisualState}"/>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using Microsoft.Maui.Controls.Core.UnitTests;
using Microsoft.Maui.Devices;
using Xunit;

using static Microsoft.Maui.Controls.Xaml.UnitTests.MockSourceGenerator;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui33676_VisualState : ContentPage
{
public Maui33676_VisualState() => InitializeComponent();

[Collection("Issue")]
public class Tests : IDisposable
{
MockDeviceInfo mockDeviceInfo;

public Tests() => DeviceInfo.SetCurrent(mockDeviceInfo = new MockDeviceInfo());

public void Dispose() => DeviceInfo.SetCurrent(null);

[Theory]
[XamlInflatorData]
// When a Setter inside VisualState uses OnPlatform without a Default value and the target platform
// doesn't match, SourceGen should handle it gracefully
internal void SetterInVisualStateWithOnPlatformWithoutDefaultShouldNotGenerateInvalidCode(XamlInflator inflator)
{
// Test compiling for MacCatalyst when OnPlatform only specifies Android
if (inflator == XamlInflator.SourceGen)
{
var result = CreateMauiCompilation()
.WithAdditionalSource(
"""
namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui33676_VisualState : ContentPage
{
public Maui33676_VisualState() => InitializeComponent();
}
""")
.RunMauiSourceGenerator(typeof(Maui33676_VisualState), targetFramework: "net10.0-maccatalyst");

var generated = result.GeneratedInitializeComponent();

// Check that we're NOT generating code that tries to add object to IList<Setter>
// This would happen if IValueProvider.ProvideValue() is called on a Setter with no value
Assert.DoesNotContain("((global::Microsoft.Maui.Controls.Xaml.IValueProvider)).ProvideValue", generated, StringComparison.Ordinal);

// Should not have any compilation errors
Assert.Empty(result.Diagnostics);
}
else
{
mockDeviceInfo.Platform = DevicePlatform.macOS;
var page = new Maui33676_VisualState(inflator);
Assert.NotNull(page);
}
}
}
}
Loading