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
113 changes: 113 additions & 0 deletions src/Controls/src/SourceGen/ITypeSymbolExtensions.Maui.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,119 @@ static partial class ITypeSymbolExtensions
return bpParentType.GetAllFields(name, context).FirstOrDefault(fi => fi.IsStatic && fi.Type.Equals(context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindableProperty"), SymbolEqualityComparer.Default));
}

/// <summary>
/// Checks if there's a property or field with a similar name that has a BindablePropertyAttribute.
/// This is a heuristic to support bindable properties generated by other source generators.
/// Returns the explicit property name if specified in the attribute, otherwise null.
/// </summary>
public static bool HasBindablePropertyHeuristic(this ITypeSymbol type, string propertyName, SourceGenContext context, out string? explicitPropertyName)
{
explicitPropertyName = null;

// Check if any property name variant has the BindablePropertyAttribute or AutoPropertyAttribute
foreach (var name in DerivePotentialPropertyNames(propertyName))
{
var property = type.GetAllProperties(name, context).FirstOrDefault();
if (property != null && HasBindablePropertyOrAutoPropertyAttribute(property, out var propName))
{
explicitPropertyName = propName;
return true;
}
}

// Check if any field name variant has the BindablePropertyAttribute or AutoPropertyAttribute
foreach (var name in DerivePotentialFieldNames(propertyName))
{
var field = type.GetAllFields(name, context).FirstOrDefault();
if (field != null && HasBindablePropertyOrAutoPropertyAttribute(field, out var propName))
{
explicitPropertyName = propName;
return true;
}
}

return false;
}

/// <summary>
/// Derives potential property name variations from a XAML property name.
/// </summary>
private static IEnumerable<string> DerivePotentialPropertyNames(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
yield break;

// Exact match (e.g., "Balance")
yield return propertyName;
}

/// <summary>
/// Derives potential field name variations from a XAML property name.
/// </summary>
private static IEnumerable<string> DerivePotentialFieldNames(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
yield break;

// Underscore prefix with lowercase first letter (e.g., "_balance" for "Balance")
yield return $"_{char.ToLowerInvariant(propertyName[0])}{propertyName.Substring(1)}";

// Lowercase first letter (e.g., "balance" for "Balance")
yield return $"{char.ToLowerInvariant(propertyName[0])}{propertyName.Substring(1)}";

// Uppercase first letter (e.g., "Balance" for "Balance" - uncommon but supported)
yield return propertyName;
}

/// <summary>
/// List of recognized attribute full names that indicate a bindable property will be generated.
/// Easy to extend by adding new attribute names to this list.
/// </summary>
private static readonly string[] RecognizedBindablePropertyAttributes = new[]
{
"CommunityToolkit.Maui.BindablePropertyAttribute",
"SQuan.Helpers.Maui.Mvvm.BindablePropertyAttribute",
"Maui.BindableProperty.Generator.Core.AutoBindableAttribute"
};

/// <summary>
/// Checks if a symbol has a recognized bindable property attribute.
/// If the attribute has a PropertyName parameter, returns it in the out parameter.
/// </summary>
private static bool HasBindablePropertyOrAutoPropertyAttribute(ISymbol symbol, out string? explicitPropertyName)
{
explicitPropertyName = null;

foreach (var attr in symbol.GetAttributes())
{
var attrClass = attr.AttributeClass;
if (attrClass == null)
continue;

var fullTypeName = attrClass.ToString();

// Check if the attribute is in our list of recognized attributes
if (RecognizedBindablePropertyAttributes.Contains(fullTypeName))
{
// Try to get the PropertyName named parameter
foreach (var namedArg in attr.NamedArguments)
{
if (namedArg.Key == "PropertyName" && namedArg.Value.Value is string propName)
{
explicitPropertyName = propName;
return true;
}
}

// Attribute found but no explicit PropertyName
explicitPropertyName = null;
return true;
}
}

return false;
}

static bool GetNameAndTypeRef(ref ITypeSymbol elementType, string namespaceURI, ref string localname,
SourceGenContext context, IXmlLineInfo? lineInfo)
{
Expand Down
179 changes: 121 additions & 58 deletions src/Controls/src/SourceGen/SetPropertyHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ public static void SetPropertyValue(IndentedTextWriter writer, ILocalValue paren
}

//If it's a BP and the value is BindingBase, SetBinding
if (!asCollectionItem && CanSetBinding(bpFieldSymbol, valueNode, context))
if (!asCollectionItem && CanSetBinding(bpFieldSymbol, valueNode, parentVar.Type, localName, context, out var explicitPropertyNameForBinding))
{
SetBinding(writer, parentVar, bpFieldSymbol!, valueNode, context, getNodeValue);
SetBinding(writer, parentVar, bpFieldSymbol, localName, explicitPropertyNameForBinding, valueNode, context, getNodeValue);
return;
}

//If it's a BP, SetValue
if (!asCollectionItem && CanSetValue(bpFieldSymbol, valueNode, context, getNodeValue))
if (!asCollectionItem && CanSetValue(bpFieldSymbol, valueNode, parentVar.Type, localName, context, getNodeValue, out var explicitPropertyNameForValue))
{
SetValue(writer, parentVar, bpFieldSymbol!, valueNode, context, getNodeValue);
SetValue(writer, parentVar, bpFieldSymbol, localName, explicitPropertyNameForValue, valueNode, context, getNodeValue);
return;
}

Expand Down Expand Up @@ -232,73 +232,106 @@ static void ConnectEvent(IndentedTextWriter writer, ILocalValue parentVar, strin
}
}

static bool CanSetValue(IFieldSymbol? bpFieldSymbol, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
static bool CanSetValue(IFieldSymbol? bpFieldSymbol, INode node, ITypeSymbol parentType, string localName, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue, out string? explicitPropertyName)
{
if (bpFieldSymbol == null)
return false;
if (node is ValueNode vn && vn.CanConvertTo(bpFieldSymbol, context))
return true;
if (node is not ElementNode en)
return false;
explicitPropertyName = null;

if (bpFieldSymbol != null)
{
// Normal BP case - apply existing logic
if (node is ValueNode vn && vn.CanConvertTo(bpFieldSymbol, context))
return true;
if (node is not ElementNode en)
return false;

var localVar = getNodeValue(en, context.Compilation.ObjectType);
var localVar = getNodeValue(en, context.Compilation.ObjectType);

// If it's an attached BP, there's no second chance to handle IMarkupExtensions, so we try here.
// Worst case scenario ? InvalidCastException at runtime
if (localVar.Type.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default))
return true;
// If it's an attached BP, there's no second chance to handle IMarkupExtensions, so we try here.
// Worst case scenario ? InvalidCastException at runtime
if (localVar.Type.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default))
return true;

var bpTypeAndConverter = bpFieldSymbol.GetBPTypeAndConverter(context);
if (context.Compilation.HasImplicitConversion(localVar.Type, bpTypeAndConverter?.type))
return true;
var bpTypeAndConverter = bpFieldSymbol.GetBPTypeAndConverter(context);
if (context.Compilation.HasImplicitConversion(localVar.Type, bpTypeAndConverter?.type))
return true;

if (HasDoubleImplicitConversion(localVar.Type, bpTypeAndConverter?.type, context, out _))
return true;
if (HasDoubleImplicitConversion(localVar.Type, bpTypeAndConverter?.type, context, out _))
return true;

if (HasExplicitConversion(localVar.Type, bpTypeAndConverter?.type, context))
return true;
if (HasExplicitConversion(localVar.Type, bpTypeAndConverter?.type, context))
return true;

if (localVar.Type.InheritsFrom(bpTypeAndConverter?.type!, context))
return true;
if (localVar.Type.InheritsFrom(bpTypeAndConverter?.type!, context))
return true;

if (bpFieldSymbol.Type.IsInterface() && localVar.Type.Implements(bpTypeAndConverter?.type!))
return true;
if (bpFieldSymbol.Type.IsInterface() && localVar.Type.Implements(bpTypeAndConverter?.type!))
return true;

return false;
}

// Heuristic: If BP is null but the type has a property/field with a BindablePropertyAttribute,
// assume the BP will be generated by another source generator
// Only apply this for non-BindingBase nodes (CanSetBinding handles BindingBase)
if (!string.IsNullOrEmpty(localName) && !IsBindingBaseNode(node, context))
{
return parentType.HasBindablePropertyHeuristic(localName, context, out explicitPropertyName);
}

return false;
}

static void SetValue(IndentedTextWriter writer, ILocalValue parentVar, IFieldSymbol bpFieldSymbol, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
static void SetValue(IndentedTextWriter writer, ILocalValue parentVar, IFieldSymbol? bpFieldSymbol, string localName, string? explicitPropertyName, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
{
var pType = bpFieldSymbol.GetBPTypeAndConverter(context)?.type;
// Determine bindable property name: use BP field symbol if available, otherwise use heuristic
var bpName = bpFieldSymbol != null
? bpFieldSymbol.ToFQDisplayString()
: $"{parentVar.Type.ToFQDisplayString()}.{explicitPropertyName ?? $"{localName}Property"}";

var pType = bpFieldSymbol?.GetBPTypeAndConverter(context)?.type;
var property = bpFieldSymbol == null ? parentVar.Type.GetAllProperties(localName, context).FirstOrDefault() : null;

if (node is ValueNode valueNode)
{
using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock())
{
var valueString = valueNode.ConvertTo(bpFieldSymbol, writer,context, parentVar);
writer.WriteLine($"{parentVar.ValueAccessor}.SetValue({bpFieldSymbol.ToFQDisplayString()}, {valueString});");
var valueString = bpFieldSymbol != null
? valueNode.ConvertTo(bpFieldSymbol, writer, context, parentVar)
: (property != null ? valueNode.ConvertTo(property, writer, context, parentVar) : getNodeValue(node, context.Compilation.ObjectType).ValueAccessor);
writer.WriteLine($"{parentVar.ValueAccessor}.SetValue({bpName}, {valueString});");
}
}
else if (node is ElementNode elementNode)
{
using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)node, context.ProjectItem) : PrePost.NoBlock())
{
var localVar = getNodeValue(elementNode, context.Compilation.ObjectType);
string cast = string.Empty;
var cast = string.Empty;

if (HasDoubleImplicitConversion(localVar.Type, pType, context, out var conv))
if (bpFieldSymbol != null)
{
cast = "(" + conv!.ReturnType.ToFQDisplayString() + ")";
}
else if (pType != null && !context.Compilation.HasImplicitConversion(localVar.Type, pType) && HasExplicitConversion(localVar.Type, pType, context))
{
// Only add cast if the source type is not object (object can be cast to anything at runtime)
if (!localVar.Type.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default))
// BP case: check for double implicit conversion first
if (HasDoubleImplicitConversion(localVar.Type, pType, context, out var conv))
{
cast = $"({pType.ToFQDisplayString()})";
cast = "(" + conv!.ReturnType.ToFQDisplayString() + ")";
}
else if (pType != null && !context.Compilation.HasImplicitConversion(localVar.Type, pType) && HasExplicitConversion(localVar.Type, pType, context))
{
// Only add cast if the source type is not object (object can be cast to anything at runtime)
if (!localVar.Type.Equals(context.Compilation.ObjectType, SymbolEqualityComparer.Default))
{
cast = $"({pType.ToFQDisplayString()})";
}
}
}
else if (property != null && !context.Compilation.HasImplicitConversion(localVar.Type, property.Type))
{
cast = $"({property.Type.ToFQDisplayString()})";
}

writer.WriteLine($"{parentVar.ValueAccessor}.SetValue({bpFieldSymbol.ToFQDisplayString()}, {cast}{localVar.ValueAccessor});");
writer.WriteLine($"{parentVar.ValueAccessor}.SetValue({bpName}, {cast}{localVar.ValueAccessor});");
}
}
}

static bool CanGet(ILocalValue parentVar, string localName, SourceGenContext context, out ITypeSymbol? propertyType, out IPropertySymbol? propertySymbol)
Expand Down Expand Up @@ -426,31 +459,42 @@ static void Set(IndentedTextWriter writer, ILocalValue parentVar, string localNa
}
}

static bool CanSetBinding(IFieldSymbol? bpFieldSymbol, INode node, SourceGenContext context)
static bool CanSetBinding(IFieldSymbol? bpFieldSymbol, INode node, ITypeSymbol parentType, string localName, SourceGenContext context, out string? explicitPropertyName)
{

if (bpFieldSymbol == null)
return false;
if (node is not ElementNode en)
return false;
if (!context.Variables.TryGetValue(en, out var localVariable))
explicitPropertyName = null;

// Check if it's a BindingBase node
if (!IsBindingBaseNode(node, context))
return false;

var bindingBaseSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!;

if (localVariable.Type.InheritsFrom(bindingBaseSymbol, context))
return true;

if (context.Compilation.HasImplicitConversion(localVariable.Type, bindingBaseSymbol))

// If we have a BP field symbol, we can set binding
if (bpFieldSymbol != null)
return true;


// Heuristic: If BP is null but the type has a property/field with a BindablePropertyAttribute,
// assume the BP will be generated by another source generator
if (!string.IsNullOrEmpty(localName))
return parentType.HasBindablePropertyHeuristic(localName, context, out explicitPropertyName);

return false;
}

static void SetBinding(IndentedTextWriter writer, ILocalValue parentVar, IFieldSymbol bpFieldSymbol, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
static void SetBinding(IndentedTextWriter writer, ILocalValue parentVar, IFieldSymbol? bpFieldSymbol, string localName, string? explicitPropertyName, INode node, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
{
var localVariable = getNodeValue((ElementNode)node, context.Compilation.ObjectType);
writer.WriteLine($"{parentVar.ValueAccessor}.SetBinding({bpFieldSymbol.ToFQDisplayString()}, {localVariable.ValueAccessor});");

if (bpFieldSymbol != null)
{
// Normal case: we have the BP field symbol
writer.WriteLine($"{parentVar.ValueAccessor}.SetBinding({bpFieldSymbol.ToFQDisplayString()}, {localVariable.ValueAccessor});");
}
else
{
// Heuristic case: generate SetBinding call using the expected BindableProperty name
// Use explicit property name if provided by attribute, otherwise use the default {localName}Property format
var bpName = explicitPropertyName ?? $"{localName}Property";
writer.WriteLine($"{parentVar.ValueAccessor}.SetBinding({parentVar.Type.ToFQDisplayString()}.{bpName}, {localVariable.ValueAccessor});");
}
}

static bool CanAdd(ILocalValue parentVar, string localName, IFieldSymbol? bpFieldSymbol, bool attached, INode valueNode, SourceGenContext context, NodeSGExtensions.GetNodeValueDelegate getNodeValue)
Expand Down Expand Up @@ -534,4 +578,23 @@ static void Add(IndentedTextWriter writer, ILocalValue parentVar, XmlName proper
using (context.ProjectItem.EnableLineInfo ? PrePost.NewLineInfo(writer, (IXmlLineInfo)valueNode, context.ProjectItem) : PrePost.NoBlock())
writer.WriteLine($"{parentObj}.Add(({itemType.ToFQDisplayString()}){cast}{getNodeValue(valueNode, context.Compilation.ObjectType).ValueAccessor});");
}

static bool IsBindingBaseNode(INode node, SourceGenContext context)
{
if (node is not ElementNode en)
return false;
if (!context.Variables.TryGetValue(en, out var localVariable))
return false;

var bindingBaseSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.BindingBase")!;

if (localVariable.Type.InheritsFrom(bindingBaseSymbol, context))
return true;

if (context.Compilation.HasImplicitConversion(localVariable.Type, bindingBaseSymbol))
return true;

return false;
}

}
Loading
Loading