Skip to content
2 changes: 1 addition & 1 deletion src/Controls/src/SourceGen/CompiledBindingMarkup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, ou
// TypedBinding creation
code.WriteLine($"return new global::Microsoft.Maui.Controls.Internals.TypedBinding<{binding.SourceType}, {binding.PropertyType}>(");
code.Indent++;
var targetNullValueExpression = isTemplateBinding || !propertyFlags.HasFlag(BindingPropertyFlags.FallbackValue) ? null : "extension.TargetNullValue";
var targetNullValueExpression = isTemplateBinding || !propertyFlags.HasFlag(BindingPropertyFlags.TargetNullValue) ? null : "extension.TargetNullValue";
code.WriteLine($"getter: source => ({GenerateGetterExpression(binding, sourceVariableName: "source", targetNullValueExpression)}, true),");
code.WriteLine("setter,");
code.Write("handlers: ");
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/SourceGen/KnownMarkups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ internal static bool ProvideValueForStyleSheetExtension(ElementNode markupNode,
else // sourceNode != null
{
var source = (string)(sourceNode as ValueNode)!.Value;
var uri = $"new global::System.Uri(\"{source}\", global::System.UriKind.Relative)";
var uri = $"new global::System.Uri(@\"{source}\", global::System.UriKind.Relative)";
var rootTargetPath = $"global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute.GetPathForType(typeof({context.RootType.ToFQDisplayString()}))";

var resourcePath = $"global::Microsoft.Maui.Controls.ResourceDictionary.RDSourceTypeConverter.GetResourcePath({uri}, {rootTargetPath})";
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/SourceGen/NodeSGExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ void reportDiagnostic()
reportDiagnostic();
}
if (toType.Equals(context.Compilation.GetTypeByMetadataName("System.Uri")!, SymbolEqualityComparer.Default))
return $"new global::System.Uri(\"{valueString}\", global::System.UriKind.RelativeOrAbsolute)";
return $"new global::System.Uri(@\"{valueString}\", global::System.UriKind.RelativeOrAbsolute)";

//default
return SymbolDisplay.FormatLiteral(valueString, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ static string GetResourcePath(string value, string rootTargetPath)

var uriType = context.Compilation.GetTypeByMetadataName("System.Uri")!;
var uriKindType = context.Compilation.GetTypeByMetadataName("System.UriKind")!;
writer.WriteLine($"var {uriVar} = new {uriType.ToFQDisplayString()}(\"{value};assembly={asm.Name}\", {uriKindType.ToFQDisplayString()}.RelativeOrAbsolute);");
writer.WriteLine($"var {uriVar} = new {uriType.ToFQDisplayString()}(@\"{value};assembly={asm.Name}\", {uriKindType.ToFQDisplayString()}.RelativeOrAbsolute);");

var rootTargetPath = context.ProjectItem.RelativePath!.Replace('\\', '/');

Expand All @@ -76,7 +76,7 @@ static string GetResourcePath(string value, string rootTargetPath)
else
{
var resourceDictionaryHelpersType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.Xaml.ResourceDictionaryHelpers")!;
writer.WriteLine($"{resourceDictionaryHelpersType.ToFQDisplayString()}.LoadFromSource({parentVar.ValueAccessor}, {uriVar}, \"{GetResourcePath(value, rootTargetPath)}\", typeof({context.RootType.ToFQDisplayString()}).Assembly, null);");
writer.WriteLine($"{resourceDictionaryHelpersType.ToFQDisplayString()}.LoadFromSource({parentVar.ValueAccessor}, {uriVar}, @\"{GetResourcePath(value, rootTargetPath)}\", typeof({context.RootType.ToFQDisplayString()}).Assembly, null);");
}

return uriVar;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public BugPage()

// Run generator with BOTH files at once - this creates the shared context
// that triggers the infinite loop with the buggy code
var result = RunGenerator<XamlGenerator>(compilation, expanderFile, bugFile);
var result = RunGenerator<XamlGenerator>(compilation, [expanderFile, bugFile]);

return result;
}, cts.Token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,156 @@ public class Product
Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error));
}

[Fact]
public void TargetNullValueWithNullablePathGeneratesValidCode()
{
// This test reproduces the issue from #32606 where TargetNullValue with nullable path
// generates invalid code: getter references extension.TargetNullValue but cannot be static
var xaml =
"""
<?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"
xmlns:test="clr-namespace:Test"
x:Class="Test.TestPage"
x:DataType="test:TestPage">
<Label Text="{Binding CurrentPerson.Id, TargetNullValue=0}"/>
</ContentPage>
""";

var code =
"""
#nullable enable
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Test;

[XamlProcessing(XamlInflator.SourceGen)]
public partial class TestPage : ContentPage
{
public Person? CurrentPerson { get; set; } = null;

public TestPage()
{
InitializeComponent();
}
}

public class Person
{
public int Id { get; set; }
}
""";
var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
var expected = $$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a .NET MAUI source generator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable

namespace Test;

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
public partial class TestPage
{
private partial void InitializeComponent()
{
// Fallback to Runtime inflation if the page was updated by HotReload
static string? getPathForType(global::System.Type type)
{
var assembly = type.Assembly;
foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute>(assembly))
{
if (xria.Type == type)
return xria.Path;
}
return null;
}

var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
{
AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(),
ResourcePath = getPathForType(typeof(global::Test.TestPage)),
Instance = this,
});

if (rlr?.ResourceContent != null)
{
this.InitializeComponentRuntime();
return;
}

var bindingExtension = new global::Microsoft.Maui.Controls.Xaml.BindingExtension();
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(bindingExtension!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 9);
var label = new global::Microsoft.Maui.Controls.Label();
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(label!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 3);
var __root = this;
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope();
#endif
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope);
#endif
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
label.transientNamescope = iNameScope;
#endif
#line 8 "{{testXamlFilePath}}"
bindingExtension.TargetNullValue = "0";
#line default
#line 8 "{{testXamlFilePath}}"
bindingExtension.Path = "CurrentPerson.Id";
#line default
var bindingBase = CreateTypedBindingFrom_bindingExtension(bindingExtension);
if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(bindingBase!) == null)
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(bindingBase!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 9);
label.SetBinding(global::Microsoft.Maui.Controls.Label.TextProperty, bindingBase);
#line 8 "{{testXamlFilePath}}"
__root.SetValue(global::Microsoft.Maui.Controls.ContentPage.ContentProperty, label);
#line default
static global::Microsoft.Maui.Controls.BindingBase CreateTypedBindingFrom_bindingExtension(global::Microsoft.Maui.Controls.Xaml.BindingExtension extension)
{
global::System.Action<global::Test.TestPage, int>? setter = static (source, value) =>
{
if (source.CurrentPerson is {} p0)
{
p0.Id = value;
}
};

return new global::Microsoft.Maui.Controls.Internals.TypedBinding<global::Test.TestPage, int>(
getter: source => (source.CurrentPerson?.Id ?? extension.TargetNullValue as int? ?? default, true),
setter,
handlers: new global::System.Tuple<global::System.Func<global::Test.TestPage, object?>, string>[]
{
new(static source => source, "CurrentPerson"),
new(static source => source.CurrentPerson, "Id"),
})
{
TargetNullValue = extension.TargetNullValue,
};
}


}
}

""";

var (result, generated) = RunGenerator(xaml, code);

// Verify the generated code matches expected structure
CodeIsEqual(expected, generated ?? string.Empty);
}

[Theory]
[InlineData("{Binding .}")]
[InlineData("{Binding}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,6 @@ public void LineInfoDisabledDefault()

var generatedCode = result.GeneratedTrees.Single(tree => Path.GetFileName(tree.FilePath) == "Test.xaml.xsg.cs").ToString();
var expectedFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
Assert.False(generatedCode.Contains(@$"#line 9 ""{expectedFilePath}""", StringComparison.Ordinal));
Assert.False(generatedCode.Contains(@$"#line 9 ""{expectedFilePath}""", StringComparison.Ordinal));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ public class SourceGenXamlInitializeComponentTestBase : SourceGenTestsBase
protected record AdditionalXamlFile(string Path, string Content, string? RelativePath = null, string? TargetPath = null, string? ManifestResourceName = null, string? TargetFramework = null, string? NoWarn = null, string Lineinfo = "enable")
: AdditionalFile(Text: ToAdditionalText(Path, Content), Kind: "Xaml", RelativePath: RelativePath ?? Path, TargetPath: TargetPath, ManifestResourceName: ManifestResourceName, TargetFramework: TargetFramework, NoWarn: NoWarn, LineInfo: Lineinfo);

protected (GeneratorDriverRunResult result, string? text) RunGenerator(string xaml, string code, string noWarn = "", string targetFramework = "", string? path = null, string lineinfo = "enable")
protected (GeneratorDriverRunResult result, string? text) RunGenerator(string xaml, string code, string noWarn = "", string targetFramework = "", string? path = null, string lineinfo = "enable", bool assertNoCompilationErrors = true)
{
var compilation = CreateMauiCompilation();
compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(code));
var workingDirectory = Environment.CurrentDirectory;
var xamlFile = new AdditionalXamlFile(Path.Combine(workingDirectory, path ?? "Test.xaml"), xaml, RelativePath: path ?? "Test.xaml", TargetFramework: targetFramework, NoWarn: noWarn, ManifestResourceName: $"{compilation.AssemblyName}.Test.xaml", Lineinfo: lineinfo);
var result = RunGenerator<XamlGenerator>(compilation, xamlFile);
var result = RunGenerator<XamlGenerator>(compilation, xamlFile, assertNoCompilationErrors);
var generated = result.Results.SingleOrDefault().GeneratedSources.SingleOrDefault(gs => gs.HintName.EndsWith(".xsg.cs")).SourceText?.ToString();

return (result, generated);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ private partial void InitializeComponent()

""";

var (result, generated) = RunGenerator(xaml, code);
var (result, generated) = RunGenerator(xaml, code, assertNoCompilationErrors: false);
Assert.False(result.Diagnostics.Any());
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,23 @@ public void XStaticWithClrNamespace()
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Test;
namespace Test
{
[XamlProcessing(XamlInflator.SourceGen)]
partial class TestPage : ContentPage
{
public TestPage()
{
InitializeComponent();
}
}
}

[XamlProcessing(XamlInflator.SourceGen)]
partial class TestPage : ContentPage
namespace MyApp.Routes
{
public TestPage()
public static class Route
{
InitializeComponent();
public static string Value => "TestRoute";
}
}
""";
Expand Down Expand Up @@ -110,7 +119,7 @@ private partial void InitializeComponent()

""";

var (result, generated) = RunGenerator(xaml, code);
var (result, generated) = RunGenerator(xaml, code, assertNoCompilationErrors: true);
Assert.False(result.Diagnostics.Any());
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
}
Expand All @@ -135,14 +144,23 @@ public void XStaticWithClrNamespaceAndAssembly()
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Test;
namespace Test
{
[XamlProcessing(XamlInflator.SourceGen)]
partial class TestPage : ContentPage
{
public TestPage()
{
InitializeComponent();
}
}
}

[XamlProcessing(XamlInflator.SourceGen)]
partial class TestPage : ContentPage
namespace ExternalLib.Constants
{
public TestPage()
public static class AppConstants
{
InitializeComponent();
public static string AppName => "TestApp";
}
}
""";
Expand Down Expand Up @@ -218,7 +236,7 @@ private partial void InitializeComponent()

""";

var (result, generated) = RunGenerator(xaml, code);
var (result, generated) = RunGenerator(xaml, code, assertNoCompilationErrors: true);
Assert.False(result.Diagnostics.Any());
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
}
Expand All @@ -243,14 +261,23 @@ public void XStaticWithClrNamespaceFieldAccess()
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Test;
namespace Test
{
[XamlProcessing(XamlInflator.SourceGen)]
partial class TestPage : ContentPage
{
public TestPage()
{
InitializeComponent();
}
}
}

[XamlProcessing(XamlInflator.SourceGen)]
partial class TestPage : ContentPage
namespace MyApp.Config
{
public TestPage()
public static class Settings
{
InitializeComponent();
public static double DefaultFontSize => 14.0;
}
}
""";
Expand Down Expand Up @@ -326,7 +353,7 @@ private partial void InitializeComponent()

""";

var (result, generated) = RunGenerator(xaml, code);
var (result, generated) = RunGenerator(xaml, code, assertNoCompilationErrors: true);
Assert.False(result.Diagnostics.Any());
Assert.Equal(expected, generated, ignoreLineEndingDifferences: true);
}
Expand Down
Loading
Loading