Skip to content
Merged
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
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
3 changes: 2 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
7 changes: 7 additions & 0 deletions src/Controls/src/SourceGen/MauiGResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,11 @@
<value>Event handler '{0}' with correct signature not found in type '{1}'.</value>
<comment>0 is handler name, 1 is type name</comment>
</data>
<data name="DuplicatePropertyAssignmentTitle" xml:space="preserve">
<value>Property set multiple times</value>
</data>
<data name="DuplicatePropertyAssignmentMessage" xml:space="preserve">
<value>Property '{0}' is being set multiple times. Only the last value will be used.</value>
<comment>0 is a property name (e.g., "Border.Content", "Label.Text")</comment>
</data>
</root>
4 changes: 2 additions & 2 deletions src/Controls/src/SourceGen/SetPropertyHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ static void SetValue(IndentedTextWriter writer, ILocalValue parentVar, IFieldSym
}
}

static bool CanGet(ILocalValue parentVar, string localName, SourceGenContext context, out ITypeSymbol? propertyType, out IPropertySymbol? propertySymbol)
internal static bool CanGet(ILocalValue parentVar, string localName, SourceGenContext context, out ITypeSymbol? propertyType, out IPropertySymbol? propertySymbol)
{
propertyType = null;
if ((propertySymbol = parentVar.Type.GetAllProperties(localName, context).FirstOrDefault()) == null)
Expand All @@ -546,7 +546,7 @@ static bool CanGet(ILocalValue parentVar, string localName, SourceGenContext con
return true;
}

static bool CanGetValue(ILocalValue parentVar, IFieldSymbol? bpFieldSymbol, bool attached, SourceGenContext context, out ITypeSymbol? propertyType)
internal static bool CanGetValue(ILocalValue parentVar, IFieldSymbol? bpFieldSymbol, bool attached, SourceGenContext context, out ITypeSymbol? propertyType)
{
propertyType = null;
if (bpFieldSymbol == null)
Expand Down
8 changes: 6 additions & 2 deletions src/Controls/src/SourceGen/SourceGenContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,14 @@ public void ReportDiagnostic(Diagnostic diagnostic)
var noWarn = ProjectItem?.NoWarn;
if (!string.IsNullOrEmpty(noWarn))
{
var suppressedIds = noWarn!.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
var suppressedIds = noWarn!.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var id in suppressedIds)
{
if (diagnostic.Id.Equals(id.Trim(), StringComparison.OrdinalIgnoreCase))
var code = id.Trim();
// Match full ID (e.g., "MAUIX2015") or bare numeric suffix (e.g., "2015")
if (code.Equals(diagnostic.Id, StringComparison.OrdinalIgnoreCase) ||
(diagnostic.Id.StartsWith("MAUIX", StringComparison.OrdinalIgnoreCase) &&
code == diagnostic.Id.Substring("MAUIX".Length)))
{
return; // Suppress this diagnostic
}
Expand Down
82 changes: 72 additions & 10 deletions src/Controls/src/SourceGen/Visitors/SetPropertiesVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
Expand Down Expand Up @@ -35,21 +36,72 @@ class SetPropertiesVisitor(SourceGenContext context, bool stopOnResourceDictiona
public bool SkipChildren(INode node, INode parentNode) => node is ElementNode en && en.IsLazyResource(parentNode, Context);
public bool IsResourceDictionary(ElementNode node) => node.IsResourceDictionary(Context);

// 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))
{
var propertyDisplayName = $"{parentNode.XmlType.Name}.{propertyName.LocalName}";
var location = LocationCreate(Context.ProjectItem.RelativePath!, lineInfo, propertyDisplayName);
context.ReportDiagnostic(Diagnostic.Create(Descriptors.DuplicatePropertyAssignment, location, propertyDisplayName));
}
}

/// <summary>
/// Resolves the type of an implicit content property and, when the property is a non-collection
/// non-Object type, calls <see cref="CheckForDuplicateProperty"/> to emit a diagnostic if it has
/// already been assigned.
/// </summary>
/// <param name="lookupName">XmlName used for the <c>GetBindableProperty</c> lookup (namespace may differ from <paramref name="trackingName"/>).</param>
/// <param name="trackingName">XmlName used as the deduplication key (always uses the parent element's namespace).</param>
void CheckImplicitContentForDuplicate(
ILocalValue parentVar,
ElementNode parentNode,
XmlName lookupName,
XmlName trackingName,
IXmlLineInfo lineInfo)
{
bool attached = false;
var localName = lookupName.LocalName;
var bpFieldSymbol = parentVar.Type.GetBindableProperty(lookupName.NamespaceURI, ref localName, out attached, context, lineInfo);
ITypeSymbol? propertyType = null;

bool hasProperty = (bpFieldSymbol != null && SetPropertyHelpers.CanGetValue(parentVar, bpFieldSymbol, attached, context, out propertyType))
|| SetPropertyHelpers.CanGet(parentVar, localName, context, out propertyType, out _);

bool isObject = propertyType != null && propertyType.SpecialType == SpecialType.System_Object;
if (hasProperty && propertyType != null && !isObject && !propertyType.CanAdd(context))
CheckForDuplicateProperty(parentNode, trackingName, lineInfo);
}

public void Visit(ValueNode node, INode parentNode)
{
//TODO support Label text as element

// if it's implicit content property, get the content property name
bool isImplicitContentProperty = false;
ILocalValue? parentVar = null;
if (!node.TryGetPropertyName(parentNode, out XmlName propertyName))
{
if (!parentNode.IsCollectionItem(node))
return;
string? contentProperty;
if (!Context.Variables.ContainsKey((ElementNode)parentNode))
return;
var parentVariable = Context.Variables[(ElementNode)parentNode];
if ((contentProperty = parentVariable.Type.GetContentPropertyName(context)) != null)
parentVar = Context.Variables[(ElementNode)parentNode];
if ((contentProperty = parentVar.Type.GetContentPropertyName(context)) != null)
{
propertyName = new XmlName(((ElementNode)parentNode).NamespaceURI, contentProperty);
isImplicitContentProperty = true;
}
else
return;
}
Expand All @@ -63,13 +115,12 @@ public void Visit(ValueNode node, INode parentNode)
if (propertyName.Equals(XmlName.mcIgnorable))
return;

// Check that parent has a variable before accessing
var parentElement = (ElementNode)parentNode;
if (!Context.Variables.TryGetValue(parentElement, out var parentVar))
{
var parentKey = parentElement.Properties.TryGetValue(XmlName.xKey, out var keyNode) && keyNode is ValueNode vn ? vn.Value?.ToString() : "(none)";
throw new System.InvalidOperationException($"SetPropertiesVisitor.Visit(ValueNode): Parent '{parentElement.XmlType.Name}' x:Key='{parentKey}' not in Variables for propertyName='{propertyName}'");
}
parentVar ??= Context.Variables.TryGetValue((ElementNode)parentNode, out var pv) ? pv : null;
// Parent element not yet in Variables — can occur for markup extensions or nodes
// processed before their parent is fully initialized. Silently skip; the assignment
// will be handled through another visitor pass.
if (parentVar == null)
return;

// Try to set runtime name (for x:Name with RuntimeNameProperty attribute)
if (TrySetRuntimeName(propertyName, parentVar, node))
Expand All @@ -79,6 +130,10 @@ public void Visit(ValueNode node, INode parentNode)
if (propertyName == XmlName.xName)
return;

// Check for duplicate property assignment for implicit content properties
if (isImplicitContentProperty)
CheckImplicitContentForDuplicate(parentVar, (ElementNode)parentNode, propertyName, propertyName, node);

SetPropertyHelpers.SetPropertyValue(Writer, parentVar, propertyName, node, Context);
}

Expand Down Expand Up @@ -207,6 +262,7 @@ public void Visit(ElementNode node, INode parentNode)
var key1 = ((ElementNode)parentNode).Properties.TryGetValue(XmlName.xKey, out var kn1) && kn1 is ValueNode vn1 ? vn1.Value?.ToString() : "(none)";
throw new System.InvalidOperationException($"SetPropertiesVisitor.Visit(ElementNode) propertyName != Empty: Parent '{((ElementNode)parentNode).XmlType.Name}' x:Key='{key1}' not in Variables");
}

SetPropertyHelpers.SetPropertyValue(Writer, pVar1, propertyName, node, Context);
}
else if (parentNode.IsCollectionItem(node) && parentNode is ElementNode)
Expand All @@ -225,8 +281,14 @@ public void Visit(ElementNode node, INode parentNode)
var name = new XmlName(node.NamespaceURI, contentProperty);
if (skips.Contains(name))
return;
if (parentNode is ElementNode node1 && node1.SkipProperties.Contains(propertyName))
if (parentNode is ElementNode node1 && node1.SkipProperties.Contains(name))
return;

// Only check for duplicate property assignment if the property is not a collection
// Use parent namespace for duplicate tracking to match how explicit property assignments are keyed
var contentPropertyName = new XmlName(((ElementNode)parentNode).NamespaceURI, contentProperty);
CheckImplicitContentForDuplicate(parentVar, (ElementNode)parentNode, name, contentPropertyName, node);

SetPropertyHelpers.SetPropertyValue(Writer, parentVar, name, node, Context);
}
else if (parentVar.Type.CanAdd(context))
Expand Down
3 changes: 2 additions & 1 deletion src/Controls/src/SourceGen/xlf/MauiGResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading