From 6df5147e37adb3057eda98f0cab4a5461d90be7a Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Tue, 17 Mar 2026 13:10:48 +0100 Subject: [PATCH 1/6] Compile x:Reference bindings against resolved element type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a binding uses Source={x:Reference}, resolve the referenced element's type by walking namescopes and compile the binding against it. This enables compiled bindings for paths like Path=Text on a referenced Label. When the path can't be fully resolved (e.g. Path=BindingContext.X where BindingContext is 'object'), fall back silently to runtime binding without emitting MAUIG2045 — these bindings were never compiled before, so a new warning would be a regression. The MAUIG2045 diagnostic is moved from TryParsePath to the caller via an out parameter, letting the caller decide whether to emit it (only for x:DataType-sourced bindings, not x:Reference). Fixes #34490 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/SourceGen/CompiledBindingMarkup.cs | 17 ++-- src/Controls/src/SourceGen/KnownMarkups.cs | 82 +++++++++++++++---- .../BindingDiagnosticsTests.cs | 55 ++++++++++++- .../Xaml.UnitTests/Issues/Gh3606.xaml.cs | 7 +- 4 files changed, 136 insertions(+), 25 deletions(-) diff --git a/src/Controls/src/SourceGen/CompiledBindingMarkup.cs b/src/Controls/src/SourceGen/CompiledBindingMarkup.cs index 6e80cb3d237a..abb896df9caf 100644 --- a/src/Controls/src/SourceGen/CompiledBindingMarkup.cs +++ b/src/Controls/src/SourceGen/CompiledBindingMarkup.cs @@ -29,15 +29,17 @@ public CompiledBindingMarkup(ElementNode node, string path, ILocalValue bindingE private Location GetLocation(INode node, string? context = null) => LocationHelpers.LocationCreate(_context.ProjectItem.RelativePath!, (IXmlLineInfo)node, context ?? "Binding"); - public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, out string? newBindingExpression) + public bool TryCompileBinding(ITypeSymbol sourceType, bool isTemplateBinding, out string? newBindingExpression, out Diagnostic? propertyNotFoundDiagnostic) { newBindingExpression = null; + propertyNotFoundDiagnostic = null; if (!TryParsePath( sourceType, out ITypeSymbol? propertyType, out SetterOptions? setterOptions, - out EquatableArray? parsedPath) + out EquatableArray? parsedPath, + out propertyNotFoundDiagnostic) || propertyType is null || setterOptions is null || !parsedPath.HasValue) @@ -261,11 +263,13 @@ bool TryParsePath( ITypeSymbol sourceType, out ITypeSymbol? propertyType, out SetterOptions? setterOptions, - out EquatableArray? bindingPath) + out EquatableArray? bindingPath, + out Diagnostic? propertyNotFoundDiagnostic) { propertyType = sourceType; setterOptions = null; bindingPath = default; + propertyNotFoundDiagnostic = null; var isNullable = false; var path = _path.Trim('.', ' '); @@ -310,10 +314,9 @@ bool TryParsePath( if (!previousPartType.TryGetProperty(p, _context, out var property, out var currentPropertyType, out var isAssumedWritable) || currentPropertyType is null) { - // Report as WARNING (not Error) because the property might be generated by another source generator - // that runs after the MAUI XAML source generator. The binding will fall back to reflection at runtime. - // This allows scenarios like CommunityToolkit.Mvvm's [ObservableProperty] to work correctly. - _context.ReportDiagnostic(Diagnostic.Create(Descriptors.BindingPropertyNotFound, GetLocation(_node), p, previousPartType.ToFQDisplayString())); + // Don't emit directly — let the caller decide whether to report this. + // For x:Reference bindings, the caller silently falls back to runtime. + propertyNotFoundDiagnostic = Diagnostic.Create(Descriptors.BindingPropertyNotFound, GetLocation(_node), p, previousPartType.ToFQDisplayString()); return false; } diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs index 9c0602411cff..f01c32674124 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -343,25 +343,37 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!; ITypeSymbol? dataTypeSymbol = null; - // When Source is explicitly set (RelativeSource or x:Reference), x:DataType does not describe - // the actual source — skip compilation and fall back to runtime binding. - bool hasExplicitSource = HasExplicitBindingSource(markupNode); + // When Source is RelativeSource, the type is determined at runtime — skip compilation. + // When Source is x:Reference, resolve the referenced element's type and compile against it. + // Otherwise, use x:DataType from the current scope. + bool hasRelativeSource = HasRelativeSourceBinding(markupNode); context.Variables.TryGetValue(markupNode, out ILocalValue? extVariable); - if ( !hasExplicitSource + if ( !hasRelativeSource && extVariable is not null) { - TryGetXDataType(markupNode, context, out dataTypeSymbol); + ITypeSymbol? xRefSourceType = TryResolveXReferenceSourceType(markupNode, context); + dataTypeSymbol = xRefSourceType; + if (dataTypeSymbol is null) + TryGetXDataType(markupNode, context, out dataTypeSymbol); if (dataTypeSymbol is not null) { var compiledBindingMarkup = new CompiledBindingMarkup(markupNode, GetBindingPath(markupNode), extVariable, context); - if (compiledBindingMarkup.TryCompileBinding(dataTypeSymbol, isTemplateBinding, out string? newBindingExpression) && newBindingExpression is not null) + if (compiledBindingMarkup.TryCompileBinding(dataTypeSymbol, isTemplateBinding, out string? newBindingExpression, out Diagnostic? propertyNotFoundDiagnostic) && newBindingExpression is not null) { value = newBindingExpression; return true; } + + // Emit property-not-found diagnostic only for x:DataType-sourced bindings. + // For x:Reference bindings, silently fall back to runtime — these bindings + // were never compiled before, so emitting a new warning would be a regression. + if (propertyNotFoundDiagnostic is not null && xRefSourceType is null) + { + context.ReportDiagnostic(propertyNotFoundDiagnostic); + } } } @@ -632,29 +644,71 @@ static bool IsBindingContextBinding(ElementNode node) && propertyName.LocalName == "BindingContext"; } - // Checks if the binding has a Source property set to RelativeSource or x:Reference. - // When Source is explicitly set, x:DataType does not describe the actual binding source, + // Checks if the binding has a Source property set to RelativeSource. + // When a binding uses RelativeSource, the source type is determined at runtime, // so we should NOT compile the binding using x:DataType. - static bool HasExplicitBindingSource(ElementNode bindingNode) + static bool HasRelativeSourceBinding(ElementNode bindingNode) { - // Check if Source property exists if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode? sourceNode) && !bindingNode.Properties.TryGetValue(new XmlName(null, "Source"), out sourceNode)) { return false; } - // Check if the Source is a RelativeSourceExtension or ReferenceExtension if (sourceNode is ElementNode sourceElementNode) { return sourceElementNode.XmlType.Name is "RelativeSourceExtension" - or "RelativeSource" - or "ReferenceExtension" - or "Reference"; + or "RelativeSource"; } return false; } + + // When Source={x:Reference Name} is set on a binding, resolves the referenced element's type + // by walking namescopes (same logic as ProvideValueForReferenceExtension). + // Returns null if Source is not an x:Reference or the name cannot be resolved. + static ITypeSymbol? TryResolveXReferenceSourceType(ElementNode bindingNode, SourceGenContext context) + { + if (!bindingNode.Properties.TryGetValue(new XmlName("", "Source"), out INode? sourceNode) + && !bindingNode.Properties.TryGetValue(new XmlName(null, "Source"), out sourceNode)) + return null; + + if (sourceNode is not ElementNode refNode) + return null; + + if (refNode.XmlType.Name is not "ReferenceExtension" and not "Reference") + return null; + + // Extract the Name from the x:Reference markup + if (!refNode.Properties.TryGetValue(new XmlName("", "Name"), out INode? refNameNode) + && !refNode.Properties.TryGetValue(new XmlName(null, "Name"), out refNameNode)) + { + refNameNode = refNode.CollectionItems.Count > 0 ? refNode.CollectionItems[0] : null; + } + + if (refNameNode is not ValueNode vn || vn.Value is not string name) + return null; + + // Walk namescopes to find the referenced element's type + ElementNode? node = bindingNode; + var currentContext = context; + while (currentContext is not null && node is not null) + { + while (currentContext is not null && !currentContext.Scopes.ContainsKey(node)) + currentContext = currentContext.ParentContext; + if (currentContext is null) + break; + var namescope = currentContext.Scopes[node]; + if (namescope.namesInScope != null && namescope.namesInScope.ContainsKey(name)) + return namescope.namesInScope[name].Type; + INode n = node; + while (n.Parent is ListNode ln) + n = ln.Parent; + node = n.Parent as ElementNode; + } + + return null; + } } internal static bool ProvideValueForDataTemplateExtension(ElementNode markupNode, IndentedTextWriter writer, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate? getNodeValue, out ITypeSymbol? returnType, out string value) diff --git a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs index 61471531093e..cd520fe8e676 100644 --- a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs +++ b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs @@ -298,11 +298,62 @@ public class ItemModel .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp)); var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false); - // x:Reference bindings skip compilation entirely — no MAUIG2045 should be emitted - // for properties on the DataTemplate's x:DataType (ItemModel). + // x:Reference resolves to ContentPage; BindingContext is 'object', so SelectItemCommand + // can't be resolved statically. MAUIG2045 is suppressed for x:Reference bindings. Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045" && d.GetMessage().Contains("ItemModel", StringComparison.Ordinal)); } + [Fact] + public void BindingWithXReferenceToNonRootElement_ResolvesCorrectType() + { + var xaml = +""" + + + + + +"""; + + var csharp = +""" +namespace Test; + +public partial class TestPage : Microsoft.Maui.Controls.ContentPage { } + +public class ViewModel +{ + public System.Collections.Generic.List Items { get; set; } +} + +public class ItemModel +{ + public string Name { get; set; } +} +"""; + + var compilation = CreateMauiCompilation() + .AddSyntaxTrees(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxTree.ParseText(csharp)); + var result = RunGenerator(compilation, new AdditionalXamlFile("Test.xaml", xaml), assertNoCompilationErrors: false); + + // Path=Text resolves against Label (the x:Reference target), not ItemModel (x:DataType). + // Label.Text exists, so there should be no MAUIG2045 at all. + Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045"); + } + [Fact] public void BindingIndexerTypeUnsupported_ReportsCorrectDiagnostic() { diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Gh3606.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Gh3606.xaml.cs index 266e592b4a55..87065fa97f15 100644 --- a/src/Controls/tests/Xaml.UnitTests/Issues/Gh3606.xaml.cs +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Gh3606.xaml.cs @@ -18,12 +18,15 @@ public class Tests : IDisposable [Theory] [XamlInflatorData] - internal void BindingsWithSourceAndInvalidPathAreNotCompiled(XamlInflator inflator) + internal void BindingsWithXReferenceSourceResolveAgainstReferencedType(XamlInflator inflator) { + // Source={x:Reference page} points to ContentPage, which has a Content property. + // The binding path "Content" resolves against ContentPage, so the source generator + // compiles it instead of falling back to runtime Binding. var view = new Gh3606(inflator); var binding = view.Label.GetContext(Label.TextProperty).Bindings.GetValue(); - Assert.IsType(binding); + Assert.IsAssignableFrom(binding); } } } From 8857327d03c6c9da72e6b29208fc1fdbc5f0877f Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Mon, 30 Mar 2026 10:42:23 +0200 Subject: [PATCH 2/6] Add XC0067/MAUIX2015 warning for duplicate implicit content property assignments in XAML (#32654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!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! Fixes #3059 ## Description of Change Adds XC0067/MAUIX2006 warning diagnostic to detect when XAML **implicit content properties** are set multiple times (e.g., ``). This implementation specifically targets the issue described in #3059: detecting multiple children in single-child content properties. ## Key Changes - **Focused implementation**: Only warns for implicit content property duplicates - **Does NOT warn for**: Explicit attribute duplicates like `