diff --git a/src/Controls/src/SourceGen/KnownMarkups.cs b/src/Controls/src/SourceGen/KnownMarkups.cs index 195b6554023e..40c255621179 100644 --- a/src/Controls/src/SourceGen/KnownMarkups.cs +++ b/src/Controls/src/SourceGen/KnownMarkups.cs @@ -189,13 +189,17 @@ public static bool ProvideValueForRelativeSourceExtension(ElementNode markupNode } else if (ancestorTypeNode is ValueNode vnType) { - // Try to parse as a type name directly (without x:Type) + // Try to parse as a type name directly (without x:Type). + // Cache the resolved symbol in context.Types keyed by the ValueNode so that + // TryGetRelativeSourceAncestorType can look it up without re-resolving. var typeName = vnType.Value as string; if (!IsNullOrEmpty(typeName)) { XmlType xmlType = TypeArgumentsParser.ParseSingle(typeName!, markupNode.NamespaceResolver, markupNode as IXmlLineInfo); xmlType.TryResolveTypeSymbol(null, context.Compilation, context.XmlnsCache, context.TypeCache, out var resolvedType); ancestorTypeSymbol = resolvedType; + if (resolvedType is not null) + context.Types[vnType] = resolvedType; } } } @@ -343,20 +347,41 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde returnType = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!; ITypeSymbol? dataTypeSymbol = null; - // 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 ( !hasRelativeSource - && extVariable is not null) + if (extVariable is not null) { - ITypeSymbol? xRefSourceType = TryResolveXReferenceSourceType(markupNode, context); - dataTypeSymbol = xRefSourceType; - if (dataTypeSymbol is null) - TryGetXDataType(markupNode, context, out dataTypeSymbol); + // Determine the source type for compiled binding based on the binding's Source configuration: + // + // 1. RelativeSource with a resolvable AncestorType: use the AncestorType as the source + // type. The symbol is already registered in context.Types by + // ProvideValueForRelativeSourceExtension, enabling trim-safe TypedBinding generation. + // + // 2. RelativeSource without AncestorType (Self, TemplatedParent, or FindAncestor without + // a type): the binding source is resolved at runtime. Using x:DataType as the source + // type here would produce a compiled binding with an incorrect source type, leading to + // runtime failures. Fall through to the string-based Binding path instead. + // + // 3. x:Reference: resolve the referenced element's type and compile against it. + // + // 4. No explicit source: use x:DataType if available to produce a compiled TypedBinding. + // isAncestorTypeSource is true whenever AncestorType was present, regardless of whether + // the type resolved successfully. This prevents a BindingPropertyNotFound diagnostic from + // firing on a path that was never compiled before — even when resolution fails. + TryGetRelativeSourceAncestorType(markupNode, context, out var ancestorTypeSymbol, out bool isAncestorTypeSource); + ITypeSymbol? xRefSourceType = null; + if (ancestorTypeSymbol is not null) + { + dataTypeSymbol = ancestorTypeSymbol; + } + + if (!isAncestorTypeSource && !HasRelativeSourceBinding(markupNode)) + { + xRefSourceType = TryResolveXReferenceSourceType(markupNode, context); + dataTypeSymbol = xRefSourceType; + if (dataTypeSymbol is null) + TryGetXDataType(markupNode, context, out dataTypeSymbol); + } if (dataTypeSymbol is not null) { @@ -367,10 +392,17 @@ private static bool ProvideValueForBindingExtension(ElementNode markupNode, Inde 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) + // Emit property-not-found diagnostic when the source type was known at compile time + // but the binding path doesn't exist on that type. Specifically: + // - x:DataType bindings: always emit (existing behavior). + // - AncestorType bindings with a resolved type: emit, because the type is known and the + // path is provably wrong — consistent with x:DataType behavior. Suppress only when the + // AncestorType itself failed to resolve (ancestorTypeSymbol == null), since no type + // inference was possible. + // - x:Reference bindings: always suppress — they were never compiled before. + if (propertyNotFoundDiagnostic is not null + && xRefSourceType is null + && (!isAncestorTypeSource || ancestorTypeSymbol is not null)) { context.ReportDiagnostic(propertyNotFoundDiagnostic); } @@ -708,6 +740,69 @@ static bool HasRelativeSourceBinding(ElementNode bindingNode) return null; } + + // Checks if the binding has a Source property that is a RelativeSource extension + // with a resolvable AncestorType. If so, returns the already-resolved AncestorType + // symbol from context.Types (populated earlier by ProvideValueForRelativeSourceExtension). + // This allows AncestorType bindings to use the compiled (trim-safe) TypedBinding path. + // + // Ordering guarantee: RelativeSourceExtension is registered in GetKnownEarlyMarkupExtensions + // and BindingExtension in GetKnownLateMarkupExtensions (see NodeSGExtensions.cs). Early markup + // extensions are always resolved before late ones, so context.Types is guaranteed to already + // contain the AncestorType symbol (if resolvable) by the time this method runs — no re-resolution + // or ordering fallback is needed here. + static bool TryGetRelativeSourceAncestorType(ElementNode bindingNode, SourceGenContext context, out ITypeSymbol? ancestorType, out bool hasAncestorType) + { + ancestorType = null; + hasAncestorType = false; + + // 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 + if (sourceNode is not ElementNode relativeSourceNode + || (relativeSourceNode.XmlType.Name != "RelativeSourceExtension" + && relativeSourceNode.XmlType.Name != "RelativeSource")) + { + return false; + } + + // Find the AncestorType property on the RelativeSource node + if (!relativeSourceNode.Properties.TryGetValue(new XmlName("", "AncestorType"), out INode? ancestorTypeNode) + && !relativeSourceNode.Properties.TryGetValue(new XmlName(null, "AncestorType"), out ancestorTypeNode)) + relativeSourceNode.Properties.TryGetValue(new XmlName(XamlParser.MauiUri, "AncestorType"), out ancestorTypeNode); + + if (ancestorTypeNode is null) + { + return false; + } + + // AncestorType node is present — mark the attempt regardless of resolution outcome. + hasAncestorType = true; + + // The AncestorType is typically an x:Type extension (ElementNode). + // ProvideValueForRelativeSourceExtension already resolved this type + // and registered it in context.Types — just look it up. + if (ancestorTypeNode is ElementNode typeExtNode) + { + return context.Types.TryGetValue(typeExtNode, out ancestorType) && ancestorType is not null; + } + + // AncestorType may also be a bare string (ValueNode), e.g. AncestorType="local:MyViewModel". + // ProvideValueForRelativeSourceExtension resolves this form and caches the result in + // context.Types, so reuse that cached value here to avoid duplicating resolution logic. + if (ancestorTypeNode is ValueNode vnType) + { + context.Types.TryGetValue(vnType, out ancestorType); + return ancestorType is not null; + } + + return false; + } } 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 cd520fe8e676..cc6038f66480 100644 --- a/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs +++ b/src/Controls/tests/SourceGen.UnitTests/BindingDiagnosticsTests.cs @@ -354,6 +354,95 @@ public class ItemModel Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045"); } + [Fact] + public void BindingWithRelativeSourceAncestorTypeInvalidPath_ReportsPropertyNotFound() + { + var xaml = +""" + + + + + + + +"""; + + var csharp = +""" +namespace Test; + +public partial class TestPage : Microsoft.Maui.Controls.ContentPage { } + +public class ViewModel +{ + 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); + + // AncestorType=TestPage is resolvable at compile time, so the path is provably wrong on + // that type — MAUIG2045 must fire, consistent with x:DataType binding behavior. + var diagnostic = result.Diagnostics.FirstOrDefault(d => d.Id == "MAUIG2045"); + Assert.NotNull(diagnostic); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + + var message = diagnostic.GetMessage(); + Assert.Contains("NonExistentProperty", message, System.StringComparison.Ordinal); + Assert.Contains("TestPage", message, System.StringComparison.Ordinal); + } + + [Fact] + public void BindingWithRelativeSourceUnresolvedAncestorTypeInvalidPath_SuppressesPropertyNotFound() + { + var xaml = +""" + + + + + + + +"""; + + var csharp = +""" +namespace Test; + +public partial class TestPage : Microsoft.Maui.Controls.ContentPage { } + +public class ViewModel +{ + 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); + + // AncestorType="NonExistentAncestorType" cannot be resolved to a type, so no type inference + // was possible for this binding — MAUIG2045 must stay suppressed even though Path is invalid, + // since this binding was never compiled before (it always fell back to runtime Binding). + Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MAUIG2045"); + } + [Fact] public void BindingIndexerTypeUnsupported_ReportsCorrectDiagnostic() { diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml new file mode 100644 index 000000000000..b6d6aab958f6 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml @@ -0,0 +1,62 @@ + + + + + + + +