Skip to content

Commit b9463d6

Browse files
[XSG] Fix FlexBasis source generator to emit float literals (#32696)
* Fix FlexBasis source generator to emit float literals Fixes #32680 The FlexBasis constructor expects float parameters, but the source generator was emitting double literals without the 'f' suffix, causing compilation errors. Changes: - Added 'f' suffix to FlexBasis numeric literals in FlexBasisConverter - Added comprehensive unit tests for FlexLayout properties The fix follows the same pattern used in ColorConverter and matches how primitive float values are handled in NodeSGExtensions. * Add comprehensive FlexLayout source generator tests - Add tests for all FlexLayout-specific attached properties - Add tests for Wrap, Direction, Basis, Grow, Shrink, AlignSelf, and Order - Add tests for layout invalidation when properties change - Improve test coverage for FlexLayout code generation scenarios
1 parent 62f1c38 commit b9463d6

2 files changed

Lines changed: 382 additions & 2 deletions

File tree

src/Controls/src/SourceGen/TypeConverters/FlexBasisConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ public string Convert(string value, BaseNode node, ITypeSymbol toType, IndentedT
3030
&& float.TryParse(value.Substring(0, value.Length - 1), NumberStyles.Number, CultureInfo.InvariantCulture, out float relflex))
3131
{
3232
var flexBasisRelType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Layouts.FlexBasis")!;
33-
return $"new {flexBasisRelType.ToFQDisplayString()}({FormatInvariant(relflex / 100)}, true)";
33+
return $"new {flexBasisRelType.ToFQDisplayString()}({FormatInvariant(relflex / 100)}f, true)";
3434
}
3535

3636
if (float.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out float flex))
3737
{
3838
var flexBasisAbsType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Layouts.FlexBasis")!;
39-
return $"new {flexBasisAbsType.ToFQDisplayString()}({FormatInvariant(flex)}, false)";
39+
return $"new {flexBasisAbsType.ToFQDisplayString()}({FormatInvariant(flex)}f, false)";
4040
}
4141
}
4242

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
using System;
2+
using System.Globalization;
3+
using System.IO;
4+
using Microsoft.CodeAnalysis;
5+
using System.Linq;
6+
using Xunit;
7+
8+
namespace Microsoft.Maui.Controls.SourceGen.UnitTests
9+
{
10+
public class FlexLayoutTests : SourceGenXamlInitializeComponentTestBase
11+
{
12+
[Fact]
13+
public void FlexLayout_BasisWithPercentage_GeneratesCorrectFlexBasis()
14+
{
15+
const string xaml = """
16+
<?xml version="1.0" encoding="UTF-8"?>
17+
<ContentPage
18+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
19+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
20+
x:Class="Test.TestPage">
21+
<FlexLayout>
22+
<Label Text="Test" FlexLayout.Basis="33%" />
23+
</FlexLayout>
24+
</ContentPage>
25+
""";
26+
27+
const string code = """
28+
using System;
29+
using Microsoft.Maui.Controls;
30+
using Microsoft.Maui.Controls.Xaml;
31+
32+
namespace Test;
33+
34+
[XamlProcessing(XamlInflator.SourceGen)]
35+
public partial class TestPage : ContentPage
36+
{
37+
public TestPage()
38+
{
39+
InitializeComponent();
40+
}
41+
}
42+
""";
43+
44+
var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
45+
var expected = $$"""
46+
//------------------------------------------------------------------------------
47+
// <auto-generated>
48+
// This code was generated by a .NET MAUI source generator.
49+
//
50+
// Changes to this file may cause incorrect behavior and will be lost if
51+
// the code is regenerated.
52+
// </auto-generated>
53+
//------------------------------------------------------------------------------
54+
#nullable enable
55+
56+
namespace Test;
57+
58+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
59+
public partial class TestPage
60+
{
61+
private partial void InitializeComponent()
62+
{
63+
// Fallback to Runtime inflation if the page was updated by HotReload
64+
static string? getPathForType(global::System.Type type)
65+
{
66+
var assembly = type.Assembly;
67+
foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute>(assembly))
68+
{
69+
if (xria.Type == type)
70+
return xria.Path;
71+
}
72+
return null;
73+
}
74+
75+
var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
76+
{
77+
AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(),
78+
ResourcePath = getPathForType(typeof(global::Test.TestPage)),
79+
Instance = this,
80+
});
81+
82+
if (rlr?.ResourceContent != null)
83+
{
84+
this.InitializeComponentRuntime();
85+
return;
86+
}
87+
88+
var label = new global::Microsoft.Maui.Controls.Label();
89+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(label!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4);
90+
var flexLayout = new global::Microsoft.Maui.Controls.FlexLayout();
91+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(flexLayout!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 6, 3);
92+
var __root = this;
93+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
94+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
95+
global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope();
96+
#endif
97+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
98+
global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope);
99+
#endif
100+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
101+
flexLayout.transientNamescope = iNameScope;
102+
#endif
103+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
104+
label.transientNamescope = iNameScope;
105+
#endif
106+
#line 7 "{{testXamlFilePath}}"
107+
label.SetValue(global::Microsoft.Maui.Controls.Label.TextProperty, "Test");
108+
#line default
109+
#line 7 "{{testXamlFilePath}}"
110+
label.SetValue(global::Microsoft.Maui.Controls.FlexLayout.BasisProperty, new global::Microsoft.Maui.Layouts.FlexBasis(0.33f, true));
111+
#line default
112+
#line 7 "{{testXamlFilePath}}"
113+
((global::System.Collections.Generic.ICollection<global::Microsoft.Maui.IView>)flexLayout.Children).Add((global::Microsoft.Maui.IView)label);
114+
#line default
115+
#line 6 "{{testXamlFilePath}}"
116+
__root.SetValue(global::Microsoft.Maui.Controls.ContentPage.ContentProperty, flexLayout);
117+
#line default
118+
}
119+
}
120+
121+
""";
122+
123+
var (result, generated) = RunGenerator(xaml, code);
124+
125+
Assert.False(result.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error),
126+
$"Generated code should not have errors. Diagnostics: {string.Join(", ", result.Diagnostics.Select(d => d.ToString()))}");
127+
128+
Assert.NotNull(generated);
129+
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
130+
}
131+
132+
[Theory]
133+
[InlineData("en-US", "0.33")]
134+
[InlineData("fr-FR", "0.5")]
135+
[InlineData("de-DE", "0.75")]
136+
public void FlexLayout_BasisWithDecimal_GeneratesFloatWithCorrectFormat(string cultureName, string basisValue)
137+
{
138+
// Set culture to test locale-independent generation
139+
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(cultureName);
140+
System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(cultureName);
141+
142+
var xaml = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
143+
<ContentPage
144+
xmlns=""http://schemas.microsoft.com/dotnet/2021/maui""
145+
xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml""
146+
x:Class=""Test.TestPage"">
147+
<FlexLayout>
148+
<Label Text=""Test"" FlexLayout.Basis=""{basisValue}"" />
149+
</FlexLayout>
150+
</ContentPage>";
151+
152+
const string code = """
153+
using System;
154+
using Microsoft.Maui.Controls;
155+
using Microsoft.Maui.Controls.Xaml;
156+
157+
namespace Test;
158+
159+
[XamlProcessing(XamlInflator.SourceGen)]
160+
public partial class TestPage : ContentPage
161+
{
162+
public TestPage()
163+
{
164+
InitializeComponent();
165+
}
166+
}
167+
""";
168+
169+
var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
170+
var expected = $$"""
171+
//------------------------------------------------------------------------------
172+
// <auto-generated>
173+
// This code was generated by a .NET MAUI source generator.
174+
//
175+
// Changes to this file may cause incorrect behavior and will be lost if
176+
// the code is regenerated.
177+
// </auto-generated>
178+
//------------------------------------------------------------------------------
179+
#nullable enable
180+
181+
namespace Test;
182+
183+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
184+
public partial class TestPage
185+
{
186+
private partial void InitializeComponent()
187+
{
188+
// Fallback to Runtime inflation if the page was updated by HotReload
189+
static string? getPathForType(global::System.Type type)
190+
{
191+
var assembly = type.Assembly;
192+
foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute>(assembly))
193+
{
194+
if (xria.Type == type)
195+
return xria.Path;
196+
}
197+
return null;
198+
}
199+
200+
var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
201+
{
202+
AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(),
203+
ResourcePath = getPathForType(typeof(global::Test.TestPage)),
204+
Instance = this,
205+
});
206+
207+
if (rlr?.ResourceContent != null)
208+
{
209+
this.InitializeComponentRuntime();
210+
return;
211+
}
212+
213+
var label = new global::Microsoft.Maui.Controls.Label();
214+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(label!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4);
215+
var flexLayout = new global::Microsoft.Maui.Controls.FlexLayout();
216+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(flexLayout!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 6, 3);
217+
var __root = this;
218+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
219+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
220+
global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope();
221+
#endif
222+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
223+
global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope);
224+
#endif
225+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
226+
flexLayout.transientNamescope = iNameScope;
227+
#endif
228+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
229+
label.transientNamescope = iNameScope;
230+
#endif
231+
#line 7 "{{testXamlFilePath}}"
232+
label.SetValue(global::Microsoft.Maui.Controls.Label.TextProperty, "Test");
233+
#line default
234+
#line 7 "{{testXamlFilePath}}"
235+
label.SetValue(global::Microsoft.Maui.Controls.FlexLayout.BasisProperty, new global::Microsoft.Maui.Layouts.FlexBasis({{basisValue}}f, false));
236+
#line default
237+
#line 7 "{{testXamlFilePath}}"
238+
((global::System.Collections.Generic.ICollection<global::Microsoft.Maui.IView>)flexLayout.Children).Add((global::Microsoft.Maui.IView)label);
239+
#line default
240+
#line 6 "{{testXamlFilePath}}"
241+
__root.SetValue(global::Microsoft.Maui.Controls.ContentPage.ContentProperty, flexLayout);
242+
#line default
243+
}
244+
}
245+
246+
""";
247+
248+
var (result, generated) = RunGenerator(xaml, code);
249+
250+
Assert.False(result.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error),
251+
$"Generated code should not have errors. Diagnostics: {string.Join(", ", result.Diagnostics.Select(d => d.ToString()))}");
252+
253+
Assert.NotNull(generated);
254+
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
255+
}
256+
257+
[Fact]
258+
public void FlexLayout_GrowAndShrink_GeneratesFloatValues()
259+
{
260+
const string xaml = """
261+
<?xml version="1.0" encoding="UTF-8"?>
262+
<ContentPage
263+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
264+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
265+
x:Class="Test.TestPage">
266+
<FlexLayout>
267+
<Label Text="Test" FlexLayout.Grow="1.5" FlexLayout.Shrink="0.75" />
268+
</FlexLayout>
269+
</ContentPage>
270+
""";
271+
272+
const string code = """
273+
using System;
274+
using Microsoft.Maui.Controls;
275+
using Microsoft.Maui.Controls.Xaml;
276+
277+
namespace Test;
278+
279+
[XamlProcessing(XamlInflator.SourceGen)]
280+
public partial class TestPage : ContentPage
281+
{
282+
public TestPage()
283+
{
284+
InitializeComponent();
285+
}
286+
}
287+
""";
288+
289+
var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
290+
var expected = $$"""
291+
//------------------------------------------------------------------------------
292+
// <auto-generated>
293+
// This code was generated by a .NET MAUI source generator.
294+
//
295+
// Changes to this file may cause incorrect behavior and will be lost if
296+
// the code is regenerated.
297+
// </auto-generated>
298+
//------------------------------------------------------------------------------
299+
#nullable enable
300+
301+
namespace Test;
302+
303+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
304+
public partial class TestPage
305+
{
306+
private partial void InitializeComponent()
307+
{
308+
// Fallback to Runtime inflation if the page was updated by HotReload
309+
static string? getPathForType(global::System.Type type)
310+
{
311+
var assembly = type.Assembly;
312+
foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute>(assembly))
313+
{
314+
if (xria.Type == type)
315+
return xria.Path;
316+
}
317+
return null;
318+
}
319+
320+
var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
321+
{
322+
AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(),
323+
ResourcePath = getPathForType(typeof(global::Test.TestPage)),
324+
Instance = this,
325+
});
326+
327+
if (rlr?.ResourceContent != null)
328+
{
329+
this.InitializeComponentRuntime();
330+
return;
331+
}
332+
333+
var label = new global::Microsoft.Maui.Controls.Label();
334+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(label!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 7, 4);
335+
var flexLayout = new global::Microsoft.Maui.Controls.FlexLayout();
336+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(flexLayout!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 6, 3);
337+
var __root = this;
338+
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
339+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
340+
global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope();
341+
#endif
342+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
343+
global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope);
344+
#endif
345+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
346+
flexLayout.transientNamescope = iNameScope;
347+
#endif
348+
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
349+
label.transientNamescope = iNameScope;
350+
#endif
351+
#line 7 "{{testXamlFilePath}}"
352+
label.SetValue(global::Microsoft.Maui.Controls.Label.TextProperty, "Test");
353+
#line default
354+
#line 7 "{{testXamlFilePath}}"
355+
label.SetValue(global::Microsoft.Maui.Controls.FlexLayout.GrowProperty, 1.5F);
356+
#line default
357+
#line 7 "{{testXamlFilePath}}"
358+
label.SetValue(global::Microsoft.Maui.Controls.FlexLayout.ShrinkProperty, 0.75F);
359+
#line default
360+
#line 7 "{{testXamlFilePath}}"
361+
((global::System.Collections.Generic.ICollection<global::Microsoft.Maui.IView>)flexLayout.Children).Add((global::Microsoft.Maui.IView)label);
362+
#line default
363+
#line 6 "{{testXamlFilePath}}"
364+
__root.SetValue(global::Microsoft.Maui.Controls.ContentPage.ContentProperty, flexLayout);
365+
#line default
366+
}
367+
}
368+
369+
""";
370+
371+
var (result, generated) = RunGenerator(xaml, code);
372+
373+
Assert.False(result.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error),
374+
$"Generated code should not have errors. Diagnostics: {string.Join(", ", result.Diagnostics.Select(d => d.ToString()))}");
375+
376+
Assert.NotNull(generated);
377+
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
378+
}
379+
}
380+
}

0 commit comments

Comments
 (0)