diff --git a/src/Controls/src/SourceGen/NodeSGExtensions.cs b/src/Controls/src/SourceGen/NodeSGExtensions.cs index 034c23953fa8..1f51c10eea38 100644 --- a/src/Controls/src/SourceGen/NodeSGExtensions.cs +++ b/src/Controls/src/SourceGen/NodeSGExtensions.cs @@ -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); diff --git a/src/Controls/src/SourceGen/SetPropertyHelpers.cs b/src/Controls/src/SourceGen/SetPropertyHelpers.cs index 0805bd2d48c0..b7d7fa91455e 100644 --- a/src/Controls/src/SourceGen/SetPropertyHelpers.cs +++ b/src/Controls/src/SourceGen/SetPropertyHelpers.cs @@ -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)); } diff --git a/src/Controls/src/SourceGen/SetterValueProvider.cs b/src/Controls/src/SourceGen/SetterValueProvider.cs index 7171e5e27ded..130a4b2adb33 100644 --- a/src/Controls/src/SourceGen/SetterValueProvider.cs +++ b/src/Controls/src/SourceGen/SetterValueProvider.cs @@ -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; @@ -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")]; diff --git a/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs b/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs index a933c0cf13f7..29e57c245292 100644 --- a/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs +++ b/src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs @@ -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 { @@ -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 diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676.xaml new file mode 100644 index 000000000000..e9c7b4c24c3e --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676.xaml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676.xaml.cs new file mode 100644 index 000000000000..08c227f76b73 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676.xaml.cs @@ -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); + } + } + } +} diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676_VisualState.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676_VisualState.xaml new file mode 100644 index 000000000000..73365c76939c --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui33676_VisualState.xaml @@ -0,0 +1,25 @@ + + + + #0D0D0D + #E0E0E0 + + +