Skip to content

Commit 7015af8

Browse files
StephaneDelcroixPureWeen
authored andcommitted
[XSG] Fix NaN value in XAML generating invalid code (#33533)
<!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ## Description Fixes #33532 When a XAML file contains `NaN` as a value (e.g., `Padding="NaN"`), the XAML Source Generator was generating invalid C# code using bare `NaN` instead of `double.NaN`, resulting in: ``` error CS0103: The name 'NaN' does not exist in the current context ``` ### Root Cause The `FormatInvariant()` helper method in `GeneratorHelpers.cs` uses `SymbolDisplay.FormatPrimitive()` which outputs just `"NaN"` for `double.NaN` values, without the required type prefix. ### Fix Updated `FormatInvariant()` to explicitly check for special floating-point values (`NaN`, `PositiveInfinity`, `NegativeInfinity`) for both `double` and `float` types, and return the properly qualified C# literals (e.g., `double.NaN`, `float.PositiveInfinity`). ## Changes ### Modified - `src/Controls/src/SourceGen/GeneratorHelpers.cs` - Added special handling for NaN and Infinity values ### Added Tests - `src/Controls/tests/SourceGen.UnitTests/Maui33532Tests.cs` - 5 SourceGen unit tests for ThicknessConverter with NaN - `src/Controls/tests/Xaml.UnitTests/Issues/Maui33532.xaml[.cs]` - XAML unit test for all inflators ## Testing All new tests pass: - `ThicknessWithSingleNaNValue` - Tests `Padding="NaN"` - `ThicknessWithTwoNaNValues` - Tests `Padding="NaN,NaN"` - `ThicknessWithFourNaNValues` - Tests `Padding="NaN,NaN,NaN,NaN"` - `ThicknessWithMixedNaNAndRegularValues` - Tests `Padding="NaN,10,NaN,20"` - `MarginWithNaNValue` - Tests `Margin="NaN"`
1 parent 6d41d0c commit 7015af8

File tree

4 files changed

+291
-2
lines changed

4 files changed

+291
-2
lines changed

src/Controls/src/SourceGen/GeneratorHelpers.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,14 +393,34 @@ public static (string? source, XamlProjectItemForCB? xamlItem, IList<Diagnostic>
393393

394394
/// <summary>
395395
/// Formats a value as a culture-independent C# literal for source generation.
396-
/// Uses SymbolDisplay.FormatPrimitive to ensure proper handling of special values like NaN and Infinity
397-
/// but also numeric types and makes sure they are formatted correctly.
396+
/// Handles special floating-point values (NaN, Infinity) and uses SymbolDisplay.FormatPrimitive
397+
/// for regular numeric types to ensure they are formatted correctly.
398398
/// </summary>
399399
/// <param name="value">The value to format</param>
400400
/// <param name="quoted">Whether to include quotes around the formatted value</param>
401401
/// <returns>A culture-independent string representation suitable for source generation</returns>
402402
public static string FormatInvariant(object value, bool quoted = false)
403403
{
404+
// Handle special floating-point values that SymbolDisplay.FormatPrimitive doesn't prefix correctly
405+
if (value is double d)
406+
{
407+
if (double.IsNaN(d))
408+
return "double.NaN";
409+
if (double.IsPositiveInfinity(d))
410+
return "double.PositiveInfinity";
411+
if (double.IsNegativeInfinity(d))
412+
return "double.NegativeInfinity";
413+
}
414+
else if (value is float f)
415+
{
416+
if (float.IsNaN(f))
417+
return "float.NaN";
418+
if (float.IsPositiveInfinity(f))
419+
return "float.PositiveInfinity";
420+
if (float.IsNegativeInfinity(f))
421+
return "float.NegativeInfinity";
422+
}
423+
404424
return SymbolDisplay.FormatPrimitive(value, quoteStrings: quoted, useHexadecimalNumbers: false);
405425
}
406426

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
using System;
2+
using System.Linq;
3+
using Xunit;
4+
5+
namespace Microsoft.Maui.Controls.SourceGen.UnitTests;
6+
7+
/// <summary>
8+
/// Tests for issue #33532: NaN value in XAML generates invalid code
9+
/// When Padding="NaN" is used, the source generator was generating bare "NaN" instead of "double.NaN"
10+
/// </summary>
11+
public class Maui33532Tests : SourceGenXamlInitializeComponentTestBase
12+
{
13+
[Fact]
14+
public void ThicknessWithSingleNaNValue()
15+
{
16+
// Issue #33532: Padding="NaN" generates invalid code with bare "NaN" instead of "double.NaN"
17+
var xaml =
18+
"""
19+
<?xml version="1.0" encoding="UTF-8"?>
20+
<ContentPage
21+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
22+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
23+
x:Class="Test.TestPage">
24+
<Button x:Name="testButton" Padding="NaN" Text="Test"/>
25+
</ContentPage>
26+
""";
27+
28+
var code =
29+
"""
30+
using Microsoft.Maui.Controls;
31+
using Microsoft.Maui.Controls.Xaml;
32+
33+
namespace Test;
34+
35+
[XamlProcessing(XamlInflator.SourceGen)]
36+
public partial class TestPage : ContentPage
37+
{
38+
public TestPage()
39+
{
40+
InitializeComponent();
41+
}
42+
}
43+
""";
44+
45+
var (result, generated) = RunGenerator(xaml, code, targetFramework: "net10.0");
46+
Assert.False(result.Diagnostics.Any());
47+
48+
// Verify that NaN is correctly generated as double.NaN
49+
Assert.Contains("double.NaN", generated, StringComparison.Ordinal);
50+
51+
// Ensure incorrect bare NaN is not generated (would fail with CS0103)
52+
// The pattern "new global::Microsoft.Maui.Thickness(NaN)" would be incorrect
53+
Assert.DoesNotContain("Thickness(NaN)", generated, StringComparison.Ordinal);
54+
}
55+
56+
[Fact]
57+
public void ThicknessWithTwoNaNValues()
58+
{
59+
// Test Padding="NaN,NaN" (horizontal, vertical)
60+
var xaml =
61+
"""
62+
<?xml version="1.0" encoding="UTF-8"?>
63+
<ContentPage
64+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
65+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
66+
x:Class="Test.TestPage">
67+
<Button x:Name="testButton" Padding="NaN,NaN" Text="Test"/>
68+
</ContentPage>
69+
""";
70+
71+
var code =
72+
"""
73+
using Microsoft.Maui.Controls;
74+
using Microsoft.Maui.Controls.Xaml;
75+
76+
namespace Test;
77+
78+
[XamlProcessing(XamlInflator.SourceGen)]
79+
public partial class TestPage : ContentPage
80+
{
81+
public TestPage()
82+
{
83+
InitializeComponent();
84+
}
85+
}
86+
""";
87+
88+
var (result, generated) = RunGenerator(xaml, code, targetFramework: "net10.0");
89+
Assert.False(result.Diagnostics.Any());
90+
91+
// Verify that NaN is correctly generated as double.NaN
92+
Assert.Contains("double.NaN", generated, StringComparison.Ordinal);
93+
}
94+
95+
[Fact]
96+
public void ThicknessWithFourNaNValues()
97+
{
98+
// Test Padding="NaN,NaN,NaN,NaN" (left, top, right, bottom)
99+
var xaml =
100+
"""
101+
<?xml version="1.0" encoding="UTF-8"?>
102+
<ContentPage
103+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
104+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
105+
x:Class="Test.TestPage">
106+
<Button x:Name="testButton" Padding="NaN,NaN,NaN,NaN" Text="Test"/>
107+
</ContentPage>
108+
""";
109+
110+
var code =
111+
"""
112+
using Microsoft.Maui.Controls;
113+
using Microsoft.Maui.Controls.Xaml;
114+
115+
namespace Test;
116+
117+
[XamlProcessing(XamlInflator.SourceGen)]
118+
public partial class TestPage : ContentPage
119+
{
120+
public TestPage()
121+
{
122+
InitializeComponent();
123+
}
124+
}
125+
""";
126+
127+
var (result, generated) = RunGenerator(xaml, code, targetFramework: "net10.0");
128+
Assert.False(result.Diagnostics.Any());
129+
130+
// Verify that NaN is correctly generated as double.NaN (should appear 4 times)
131+
Assert.Contains("double.NaN", generated, StringComparison.Ordinal);
132+
}
133+
134+
[Fact]
135+
public void ThicknessWithMixedNaNAndRegularValues()
136+
{
137+
// Test mixing NaN with regular values: Padding="NaN,10,NaN,20"
138+
var xaml =
139+
"""
140+
<?xml version="1.0" encoding="UTF-8"?>
141+
<ContentPage
142+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
143+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
144+
x:Class="Test.TestPage">
145+
<Button x:Name="testButton" Padding="NaN,10,NaN,20" Text="Test"/>
146+
</ContentPage>
147+
""";
148+
149+
var code =
150+
"""
151+
using Microsoft.Maui.Controls;
152+
using Microsoft.Maui.Controls.Xaml;
153+
154+
namespace Test;
155+
156+
[XamlProcessing(XamlInflator.SourceGen)]
157+
public partial class TestPage : ContentPage
158+
{
159+
public TestPage()
160+
{
161+
InitializeComponent();
162+
}
163+
}
164+
""";
165+
166+
var (result, generated) = RunGenerator(xaml, code, targetFramework: "net10.0");
167+
Assert.False(result.Diagnostics.Any());
168+
169+
// Verify that both NaN and regular values are generated correctly
170+
Assert.Contains("double.NaN", generated, StringComparison.Ordinal);
171+
Assert.Contains("Thickness", generated, StringComparison.Ordinal);
172+
}
173+
174+
[Fact]
175+
public void MarginWithNaNValue()
176+
{
177+
// Test Margin (also uses ThicknessConverter) with NaN
178+
var xaml =
179+
"""
180+
<?xml version="1.0" encoding="UTF-8"?>
181+
<ContentPage
182+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
183+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
184+
x:Class="Test.TestPage">
185+
<Label x:Name="testLabel" Margin="NaN" Text="Test"/>
186+
</ContentPage>
187+
""";
188+
189+
var code =
190+
"""
191+
using Microsoft.Maui.Controls;
192+
using Microsoft.Maui.Controls.Xaml;
193+
194+
namespace Test;
195+
196+
[XamlProcessing(XamlInflator.SourceGen)]
197+
public partial class TestPage : ContentPage
198+
{
199+
public TestPage()
200+
{
201+
InitializeComponent();
202+
}
203+
}
204+
""";
205+
206+
var (result, generated) = RunGenerator(xaml, code, targetFramework: "net10.0");
207+
Assert.False(result.Diagnostics.Any());
208+
209+
// Verify that NaN is correctly generated as double.NaN for Margin
210+
Assert.Contains("double.NaN", generated, StringComparison.Ordinal);
211+
}
212+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
4+
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui33532">
5+
<!-- Test: NaN value in XAML should generate valid code with double.NaN -->
6+
<StackLayout>
7+
<Button x:Name="buttonNaN" Padding="NaN" Text="Padding = NaN"/>
8+
<Button x:Name="buttonNaNComma" Padding="NaN,NaN" Text="Padding = NaN,NaN"/>
9+
<Button x:Name="buttonNaNFull" Padding="NaN,NaN,NaN,NaN" Text="Padding = NaN,NaN,NaN,NaN"/>
10+
</StackLayout>
11+
</ContentPage>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
using Xunit;
4+
5+
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
6+
7+
public partial class Maui33532 : ContentPage
8+
{
9+
public Maui33532() => InitializeComponent();
10+
11+
[Collection("Issue")]
12+
public class Tests
13+
{
14+
[Theory]
15+
[XamlInflatorData]
16+
internal void NaNValueInXamlGeneratesValidCode(XamlInflator inflator)
17+
{
18+
// This test reproduces issue #33532:
19+
// When a XAML file contains NaN as a value (e.g., Padding="NaN"),
20+
// the XAML Source Generator was generating invalid C# code using bare "NaN"
21+
// instead of "double.NaN", causing CS0103 compiler error.
22+
var page = new Maui33532(inflator);
23+
24+
Assert.NotNull(page);
25+
Assert.NotNull(page.buttonNaN);
26+
Assert.NotNull(page.buttonNaNComma);
27+
Assert.NotNull(page.buttonNaNFull);
28+
29+
// Verify NaN values were applied correctly (all sides should be NaN)
30+
Assert.True(double.IsNaN(page.buttonNaN.Padding.Left));
31+
Assert.True(double.IsNaN(page.buttonNaN.Padding.Top));
32+
Assert.True(double.IsNaN(page.buttonNaN.Padding.Right));
33+
Assert.True(double.IsNaN(page.buttonNaN.Padding.Bottom));
34+
35+
// Verify 2-value NaN syntax (horizontal, vertical)
36+
Assert.True(double.IsNaN(page.buttonNaNComma.Padding.Left));
37+
Assert.True(double.IsNaN(page.buttonNaNComma.Padding.Top));
38+
39+
// Verify 4-value NaN syntax
40+
Assert.True(double.IsNaN(page.buttonNaNFull.Padding.Left));
41+
Assert.True(double.IsNaN(page.buttonNaNFull.Padding.Top));
42+
Assert.True(double.IsNaN(page.buttonNaNFull.Padding.Right));
43+
Assert.True(double.IsNaN(page.buttonNaNFull.Padding.Bottom));
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)