Skip to content
71 changes: 71 additions & 0 deletions src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using Microsoft.CodeAnalysis;

namespace Microsoft.Maui.Controls.BindingSourceGen;
Expand Down Expand Up @@ -43,4 +44,74 @@ private static string GetGlobalName(this ITypeSymbol typeSymbol, bool isNullable

return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}

/// <summary>
/// Checks if a property name could be generated by CommunityToolkit.Mvvm's [RelayCommand] attribute,
/// and returns the inferred command type if found.
/// </summary>
/// <param name="symbol">The type to search</param>
/// <param name="propertyName">The name of the property to find (should end with "Command")</param>
/// <param name="compilation">The compilation (can be null)</param>
/// <param name="commandType">The inferred ICommand type if a RelayCommand method is found</param>
/// <returns>True if a RelayCommand method was found that would generate this property</returns>
public static bool TryGetRelayCommandPropertyType(this ITypeSymbol symbol, string propertyName, Compilation? compilation, out ITypeSymbol? commandType)
{
commandType = null;

if (compilation == null)
return false;

// Check if the property name ends with "Command"
if (!propertyName.EndsWith("Command", System.StringComparison.Ordinal))
return false;

// Extract the method name (property name without "Command" suffix)
var methodName = propertyName.Substring(0, propertyName.Length - "Command".Length);

Comment thread
simonrozsival marked this conversation as resolved.
// Look for a method with the base name - search in the type and base types
var methods = GetAllMethods(symbol, methodName);

foreach (var method in methods)
{
// Check if the method has the RelayCommand attribute
var hasRelayCommand = method.GetAttributes().Any(attr =>
attr.AttributeClass?.Name == "RelayCommandAttribute" ||
attr.AttributeClass?.ToDisplayString() == "CommunityToolkit.Mvvm.Input.RelayCommandAttribute");

if (hasRelayCommand)
{
// Try to find the ICommand interface type
var icommandType = compilation.GetTypeByMetadataName("System.Windows.Input.ICommand");
if (icommandType != null)
{
commandType = icommandType;
return true;
}
}
}

return false;
}

private static System.Collections.Generic.IEnumerable<IMethodSymbol> GetAllMethods(ITypeSymbol symbol, string name)
{
// Search in current type
foreach (var member in symbol.GetMembers(name))
{
if (member is IMethodSymbol method)
yield return method;
}

// Search in base types
var baseType = symbol.BaseType;
while (baseType != null)
{
foreach (var member in baseType.GetMembers(name))
{
if (member is IMethodSymbol method)
yield return method;
}
baseType = baseType.BaseType;
}
}
}
35 changes: 35 additions & 0 deletions src/Controls/src/BindingSourceGen/PathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,17 @@ private Result<List<IPathPart>> HandleMemberAccessExpression(MemberAccessExpress
var typeInfo = _context.SemanticModel.GetTypeInfo(memberAccess).Type;
var symbol = _context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;

// Handle known special cases when symbol or type are not resolved at compile time
if (symbol == null || typeInfo == null)
{
// Try to infer from known patterns (e.g., RelayCommand properties)
var expressionType = _context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type;
if (expressionType != null && TryHandleSpecialCases(member, expressionType, out var specialCasePart) && specialCasePart != null)
{
result.Value.Add(specialCasePart);
return Result<List<IPathPart>>.Success(result.Value);
}

return Result<List<IPathPart>>.Failure(DiagnosticsFactory.UnableToResolvePath(memberAccess.GetLocation()));
}

Expand Down Expand Up @@ -73,6 +82,32 @@ private Result<List<IPathPart>> HandleMemberAccessExpression(MemberAccessExpress
return Result<List<IPathPart>>.Success(result.Value);
}

private bool TryHandleSpecialCases(string memberName, ITypeSymbol expressionType, out IPathPart? pathPart)
{
pathPart = null;

// Check for RelayCommand-generated properties
if (expressionType.TryGetRelayCommandPropertyType(memberName, _context.SemanticModel.Compilation, out var commandType)
&& commandType != null)
{
var memberType = commandType.CreateTypeDescription(_enabledNullable);
var containingType = expressionType.CreateTypeDescription(_enabledNullable);

pathPart = new MemberAccess(
MemberName: memberName,
IsValueType: !commandType.IsReferenceType,
ContainingType: containingType,
MemberType: memberType,
Kind: AccessorKind.Property,
IsGetterInaccessible: false, // Assume generated property is accessible
Comment thread
simonrozsival marked this conversation as resolved.
IsSetterInaccessible: true); // Commands are typically read-only

return true;
}

return false;
}

private Result<List<IPathPart>> HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess)
{
var result = ParsePath(elementAccess.Expression);
Expand Down
13 changes: 7 additions & 6 deletions src/Controls/src/SourceGen/CompiledBindingMarkup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,9 @@ bool TryParsePath(

if (p.Length > 0)
{
var property = previousPartType.GetAllProperties(p, _context).FirstOrDefault(property => property.GetMethod != null && !property.GetMethod.IsStatic);
if (property is null)
// Try to find property or infer from RelayCommand method
if (!previousPartType.TryGetPropertyOrRelayCommand(p, _context, out var property, out var currentPropertyType)
|| currentPropertyType is null)
{
return false; // TODO report diagnostic
}
Expand All @@ -319,10 +320,10 @@ bool TryParsePath(
// && a.ConstructorArguments.Length == 1
// && a.ConstructorArguments[0].Value is int nullableContextValue
// && nullableContextValue > 0);
var memberIsNullable = property.Type.IsTypeNullable(enabledNullable);
var memberIsNullable = currentPropertyType.IsTypeNullable(enabledNullable);
isNullable |= memberIsNullable;

IPathPart memberAccess = new MemberAccess(p, property.Type.IsValueType);
IPathPart memberAccess = new MemberAccess(p, currentPropertyType.IsValueType);
if (previousPartIsNullable)
{
memberAccess = new ConditionalAccess(memberAccess);
Expand All @@ -332,13 +333,13 @@ bool TryParsePath(

// TODO: do this only if it is the last part?
setterOptions = new SetterOptions(
IsWritable: property.SetMethod != null
IsWritable: property?.SetMethod != null
Comment thread
simonrozsival marked this conversation as resolved.
Outdated
&& property.SetMethod.IsPublic()
&& !property.SetMethod.IsInitOnly
&& !property.SetMethod.IsStatic,
AcceptsNullValue: memberIsNullable);

previousPartType = property.Type;
previousPartType = currentPropertyType;
previousPartIsNullable = memberIsNullable;
}

Expand Down
49 changes: 49 additions & 0 deletions src/Controls/src/SourceGen/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using Microsoft.CodeAnalysis;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Controls.BindingSourceGen;

namespace Microsoft.Maui.Controls.SourceGen;

Expand Down Expand Up @@ -83,6 +84,54 @@ public static IEnumerable<IPropertySymbol> GetAllProperties(this ITypeSymbol sym
public static IEnumerable<IPropertySymbol> GetAllProperties(this ITypeSymbol symbol, string name, SourceGenContext? context)
=> symbol.GetAllMembers(name, context).OfType<IPropertySymbol>();

/// <summary>
/// Tries to get a property by name, and if not found, checks if it could be inferred from a RelayCommand method.
/// Returns the property type if found or inferred.
/// </summary>
/// <param name="symbol">The type to search</param>
/// <param name="propertyName">The name of the property to find</param>
/// <param name="context">The source generation context</param>
/// <param name="property">The found property symbol (null if inferred from RelayCommand)</param>
/// <param name="propertyType">The property type (either from the property or inferred from RelayCommand)</param>
/// <returns>True if property exists or can be inferred from RelayCommand</returns>
public static bool TryGetPropertyOrRelayCommand(
Comment thread
simonrozsival marked this conversation as resolved.
Outdated
this ITypeSymbol symbol,
string propertyName,
SourceGenContext? context,
out IPropertySymbol? property,
out ITypeSymbol? propertyType)
{
property = symbol.GetAllProperties(propertyName, context)
.FirstOrDefault(p => p.GetMethod != null && !p.GetMethod.IsStatic);

if (property != null)
{
propertyType = property.Type;
return true;
}

// If property not found, check if it could be a RelayCommand-generated property
if (symbol.TryGetRelayCommandPropertyType(propertyName, context?.Compilation, out propertyType))
{
return true;
}

propertyType = null;
return false;
}

/// <summary>
/// Checks if a property name could be generated by CommunityToolkit.Mvvm's [RelayCommand] attribute,
/// and returns the inferred command type if found.
/// </summary>
/// <param name="symbol">The type to search</param>
/// <param name="propertyName">The name of the property to find (should end with "Command")</param>
/// <param name="context">The source generation context</param>
/// <param name="commandType">The inferred ICommand type if a RelayCommand method is found</param>
/// <returns>True if a RelayCommand method was found that would generate this property</returns>
public static bool TryGetRelayCommandPropertyType(this ITypeSymbol symbol, string propertyName, SourceGenContext? context, out ITypeSymbol? commandType)
=> symbol.TryGetRelayCommandPropertyType(propertyName, context?.Compilation, out commandType);

Comment thread
simonrozsival marked this conversation as resolved.
Outdated
public static IEnumerable<IMethodSymbol> GetAllMethods(this ITypeSymbol symbol, SourceGenContext? context)
=> symbol.GetAllMembers(context).OfType<IMethodSymbol>();
public static IEnumerable<IMethodSymbol> GetAllMethods(this ITypeSymbol symbol, string name, SourceGenContext? context)
Expand Down