Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Controls/src/Build.Tasks/BuildException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class BuildExceptionCode
public static BuildExceptionCode NamescopeDuplicate = new BuildExceptionCode("XC", 0064, nameof(NamescopeDuplicate), "");
public static BuildExceptionCode ContentPropertyAttributeMissing = new BuildExceptionCode("XC", 0065, nameof(ContentPropertyAttributeMissing), "");
public static BuildExceptionCode InvalidXaml = new BuildExceptionCode("XC", 0066, nameof(InvalidXaml), "");
public static BuildExceptionCode DuplicatePropertyAssignment = new BuildExceptionCode("XC", 0067, nameof(DuplicatePropertyAssignment), ""); //warning


//Extensions
Expand Down
6 changes: 5 additions & 1 deletion src/Controls/src/Build.Tasks/ErrorMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,12 @@
</data>
<data name="StaticResourceSyntax" xml:space="preserve">
<value>A key is required in {StaticResource}.</value>
</data>
</data>
<data name="XSharedNotSupported" xml:space="preserve">
<value>x:Shared is only supported with SourceGen inflator. Remove the attribute or switch to SourceGen.</value>
</data>
<data name="DuplicatePropertyAssignment" xml:space="preserve">
<value>Property '{0}' is being set multiple times. Only the last value will be used.</value>
<comment>0 is property name (e.g. "Border.Content" or "Label.Text")</comment>
</data>
</root>
59 changes: 57 additions & 2 deletions src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,31 @@ public bool IsResourceDictionary(ElementNode node)

ModuleDefinition Module { get; } = context.Body.Method.Module;

// Track properties that have been set to detect duplicates
readonly Dictionary<ElementNode, HashSet<XmlName>> setProperties = new Dictionary<ElementNode, HashSet<XmlName>>();

void CheckForDuplicateProperty(ElementNode parentNode, XmlName propertyName, IXmlLineInfo lineInfo)
{
if (!setProperties.TryGetValue(parentNode, out var props))
{
props = new HashSet<XmlName>();
setProperties[parentNode] = props;
}

if (!props.Add(propertyName))
{
// Property is being set multiple times
Context.LoggingHelper.LogWarningOrError(
DuplicatePropertyAssignment,
Context.XamlFilePath,
lineInfo.LineNumber,
lineInfo.LinePosition,
0,
0,
$"{parentNode.XmlType.Name}.{propertyName.LocalName}");
}
}

public void Visit(ValueNode node, INode parentNode)
{
//TODO support Label text as element
Expand All @@ -69,6 +94,12 @@ public void Visit(ValueNode node, INode parentNode)
return;
if (propertyName.Equals(XmlName.mcIgnorable))
return;

// TODO: Add duplicate implicit content property checking here to match SourceGen behavior
// (SourceGen Visit(ValueNode) emits MAUIX2015 when a ValueNode assigns the same implicit
// content property that was already assigned by an ElementNode or another ValueNode).
// Without this check, <Border>text<Label/></Border> produces MAUIX2015 under SourceGen
// but no warning under XamlC.
Context.IL.Append(SetPropertyValue(Context.Variables[(ElementNode)parentNode], propertyName, node, Context, node));
}

Expand Down Expand Up @@ -140,8 +171,32 @@ public void Visit(ElementNode node, INode parentNode)
var name = new XmlName(node.NamespaceURI, contentProperty);
if (skips.Contains(name))
return;
if (parentNode is ElementNode node2 && node2.SkipProperties.Contains(propertyName))
if (parentNode is ElementNode node2 && node2.SkipProperties.Contains(name))
return;

// Resolve the content property type to determine if it is a collection.
// Use CanGet (CLR property resolution) rather than GetBindablePropertyReference+GetValue/Get
// to avoid side effects: GetBindablePropertyReference can emit ObsoleteProperty diagnostics
// and GetValue/Get generate IL instructions; both would be duplicated by SetPropertyValue.
// CanGet covers all content properties because every BP-backed property has a CLR wrapper.
var propLocalName = name.LocalName;
bool canResolveProperty = CanGet(parentVar, propLocalName, Context, out TypeReference contentPropType);

// Skip duplicate check when the property cannot be resolved, is System.Object (unresolved
// generics would produce false positives), or is a collection type. Matches SourceGen behavior.
if (canResolveProperty && contentPropType != null)
{
bool isObject = contentPropType.FullName == "System.Object";
bool isCollection = !isObject
&& contentPropType.ImplementsInterface(Context.Cache, Module.ImportReference(Context.Cache, ("mscorlib", "System.Collections", "IEnumerable")))
&& contentPropType.GetMethods(Context.Cache, md => md.Name == "Add" && md.Parameters.Count == 1, Module).Any();

// Use parent namespace for duplicate tracking to match how explicit property assignments are keyed
var contentPropertyName = new XmlName(((ElementNode)parentNode).NamespaceURI, contentProperty);
if (!isCollection && !isObject)
CheckForDuplicateProperty((ElementNode)parentNode, contentPropertyName, node);
}

Context.IL.Append(SetPropertyValue(Context.Variables[(ElementNode)parentNode], name, node, Context, node));
}
// Collection element, implicit content, or implicit collection element.
Expand Down Expand Up @@ -539,7 +594,7 @@ static bool TryCompileBindingPath(ElementNode node, ILContext context, VariableD
// continue compilation
}

if ( dataTypeNode is ElementNode enode
if (dataTypeNode is ElementNode enode
&& enode.XmlType.NamespaceUri == XamlParser.X2009Uri
&& enode.XmlType.Name == nameof(Xaml.NullExtension))
{
Expand Down
1 change: 1 addition & 0 deletions src/Controls/src/SourceGen/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ MAUIX2010 | XamlParsing | Info | ExpressionNotSettable
MAUIX2011 | XamlParsing | Warning | AmbiguousMemberWithStaticType
MAUIX2012 | XamlParsing | Error | CSharpExpressionsRequirePreviewFeatures
MAUIX2013 | XamlParsing | Error | AsyncLambdaNotSupported
MAUIX2015 | XamlInflation | Warning | DuplicatePropertyAssignment
36 changes: 36 additions & 0 deletions src/Controls/src/SourceGen/BindingContextDataType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.CodeAnalysis;
using Microsoft.Maui.Controls.Xaml;

namespace Microsoft.Maui.Controls.SourceGen;

/// <summary>
/// Distinguishes between different states of x:DataType resolution on a node.
/// </summary>
enum BindingContextDataTypeKind
{
/// <summary>x:DataType resolved to a known type.</summary>
Resolved,

/// <summary>x:DataType="{x:Null}" — explicit opt-out of compiled bindings for this scope.</summary>
ExplicitNull,

/// <summary>x:DataType is present but couldn't be resolved (diagnostic already reported).</summary>
Unresolved,
}

/// <summary>
/// Represents the resolved x:DataType for a node in the XAML tree.
/// Pre-computed by <see cref="PropagateDataTypeVisitor"/> and stored in
/// <see cref="SourceGenContext.BindingContextDataTypes"/>.
/// </summary>
/// <param name="Kind">The resolution state.</param>
/// <param name="Symbol">The resolved type symbol. Non-null only when <paramref name="Kind"/> is <see cref="BindingContextDataTypeKind.Resolved"/>.</param>
/// <param name="Origin">The node where x:DataType was declared (for diagnostics). Null when inherited from a parent context.</param>
/// <param name="IsInherited">True if the data type was inherited from an ancestor, false if declared on this node.</param>
/// <param name="CrossedTemplateBoundary">True if the data type was inherited across a DataTemplate boundary (likely a bug).</param>
record BindingContextDataType(
BindingContextDataTypeKind Kind,
ITypeSymbol? Symbol,
INode? Origin,
bool IsInherited,
bool CrossedTemplateBoundary);
17 changes: 10 additions & 7 deletions src/Controls/src/SourceGen/CompiledBindingMarkup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IPathPart>? parsedPath)
out EquatableArray<IPathPart>? parsedPath,
out propertyNotFoundDiagnostic)
|| propertyType is null
|| setterOptions is null
|| !parsedPath.HasValue)
Expand Down Expand Up @@ -261,11 +263,13 @@ bool TryParsePath(
ITypeSymbol sourceType,
out ITypeSymbol? propertyType,
out SetterOptions? setterOptions,
out EquatableArray<IPathPart>? bindingPath)
out EquatableArray<IPathPart>? bindingPath,
out Diagnostic? propertyNotFoundDiagnostic)
{
propertyType = sourceType;
setterOptions = null;
bindingPath = default;
propertyNotFoundDiagnostic = null;

var isNullable = false;
var path = _path.Trim('.', ' ');
Expand Down Expand Up @@ -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;
}

Expand Down
8 changes: 8 additions & 0 deletions src/Controls/src/SourceGen/Descriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,14 @@ public static class Descriptors
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor DuplicatePropertyAssignment = new DiagnosticDescriptor(
id: "MAUIX2015",
title: new LocalizableResourceString(nameof(MauiGResources.DuplicatePropertyAssignmentTitle), MauiGResources.ResourceManager, typeof(MauiGResources)),
messageFormat: new LocalizableResourceString(nameof(MauiGResources.DuplicatePropertyAssignmentMessage), MauiGResources.ResourceManager, typeof(MauiGResources)),
category: "XamlInflation",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

// public static BuildExceptionCode TypeResolution = new BuildExceptionCode("XC", 0000, nameof(TypeResolution), "");
// public static BuildExceptionCode PropertyResolution = new BuildExceptionCode("XC", 0001, nameof(PropertyResolution), "");
// public static BuildExceptionCode MissingEventHandler = new BuildExceptionCode("XC", 0002, nameof(MissingEventHandler), "");
Expand Down
4 changes: 3 additions & 1 deletion src/Controls/src/SourceGen/InitializeComponentCodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ PrePost newblock() =>
return;
}

""");
"""
);
}
Visit(root, sgcontext);

Expand Down Expand Up @@ -176,6 +177,7 @@ static void Visit(RootNode rootnode, SourceGenContext visitorContext, bool useDe
rootnode.Accept(new CreateValuesVisitor(visitorContext), null);
rootnode.Accept(new SetNamescopesAndRegisterNamesVisitor(visitorContext), null); //set namescopes for {x:Reference} and FindByName
rootnode.Accept(new SetFieldsForXNamesVisitor(visitorContext), null);
rootnode.Accept(new PropagateDataTypeVisitor(visitorContext), null);
// rootnode.Accept(new FillResourceDictionariesVisitor(visitorContext), null);
rootnode.Accept(new SetResourcesVisitor(visitorContext), null);
rootnode.Accept(new SetPropertiesVisitor(visitorContext, true), null);
Expand Down
Loading
Loading