Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
107 changes: 96 additions & 11 deletions src/Controls/src/BindingSourceGen/BindingCodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,13 @@ public void AppendBindingFactoryMethod(BindingInvocationDescription binding, str
AppendLine('{');
Indent();

// For inaccessible types, use object and cast
var sourceTypeForBinding = GetTypeForBinding(binding.SourceType);
var propertyTypeForBinding = GetTypeForBinding(binding.PropertyType);

// Initialize setter
AppendLines($$"""
global::System.Action<{{binding.SourceType}}, {{binding.PropertyType}}>? setter = null;
global::System.Action<{{sourceTypeForBinding}}, {{propertyTypeForBinding}}>? setter = null;
if ({{GetShouldUseSetterCall(binding.MethodType)}})
{
""");
Expand Down Expand Up @@ -167,10 +171,10 @@ public void AppendBindingFactoryMethod(BindingInvocationDescription binding, str
AppendLine('}');
AppendBlankLine();

// Create instance of TypedBinding
// Create instance of TypedBinding - use object types for inaccessible types
AppendLines($$"""
var binding = new global::Microsoft.Maui.Controls.Internals.TypedBinding<{{binding.SourceType}}, {{binding.PropertyType}}>(
getter: source => (getter(source), true),
var binding = new global::Microsoft.Maui.Controls.Internals.TypedBinding<{{sourceTypeForBinding}}, {{propertyTypeForBinding}}>(
getter: source => ({{GenerateGetterInvocation(binding)}}),
setter,
""");
Indent();
Expand Down Expand Up @@ -200,11 +204,44 @@ public void AppendBindingFactoryMethod(BindingInvocationDescription binding, str
}

AppendUnsafeAccessors(binding);
AppendUnsafeAccessorTypes(binding);

Unindent();
AppendLine('}');
}

private static string GetTypeForBinding(TypeDescription type)
{
// Use object for inaccessible types in TypedBinding
return type.IsAccessible ? type.ToString() : "object";
}

private string GenerateGetterInvocation(BindingInvocationDescription binding)
{
// If source type is inaccessible, we need to cast from object
if (!binding.SourceType.IsAccessible)
{
// Cast to the actual type to invoke the getter, then cast result if needed
var castSource = $"({binding.SourceType})source";
var getterCall = $"getter({castSource})";

// If property type is also inaccessible, cast the result
if (!binding.PropertyType.IsAccessible)
{
return $"(object){getterCall}, true";
}
return $"{getterCall}, true";
}
else if (!binding.PropertyType.IsAccessible)
{
// Only property type is inaccessible, cast result
return $"(object)getter(source), true";
}

// Both types accessible, no casting needed
return "getter(source), true";
}

private void AppendFunctionArguments(BindingInvocationDescription binding)
{
AppendLine('(');
Expand All @@ -218,8 +255,17 @@ private void AppendFunctionArguments(BindingInvocationDescription binding)
""");
}

// Use UnsafeAccessorType for inaccessible types
var sourceTypeForSignature = GetTypeForSignature(binding.SourceType);
var propertyTypeForSignature = GetTypeForSignature(binding.PropertyType);

if (!binding.SourceType.IsAccessible && binding.SourceType.AssemblyQualifiedName != null)
{
AppendLine($"[global::System.Runtime.CompilerServices.UnsafeAccessorType(\"{binding.SourceType.AssemblyQualifiedName}\")]");
}

AppendLines($$"""
global::System.Func<{{binding.SourceType}}, {{binding.PropertyType}}> getter,
global::System.Func<{{sourceTypeForSignature}}, {{propertyTypeForSignature}}> getter,
global::Microsoft.Maui.Controls.BindingMode mode = global::Microsoft.Maui.Controls.BindingMode.Default,
global::Microsoft.Maui.Controls.IValueConverter? converter = null,
object? converterParameter = null,
Expand All @@ -232,6 +278,12 @@ private void AppendFunctionArguments(BindingInvocationDescription binding)
Unindent();
}

private static string GetTypeForSignature(TypeDescription type)
{
// Use object for inaccessible types in signatures
return type.IsAccessible ? type.ToString() : "object";
}

private static string GetShouldUseSetterCall(InterceptedMethodType interceptedMethodType) =>
interceptedMethodType switch
{
Expand Down Expand Up @@ -265,19 +317,37 @@ private void AppendSetterLambda(BindingInvocationDescription binding, string sou
AppendLine('{');
Indent();

// Cast source if it's an inaccessible type
var actualSourceVar = sourceVariableName;
if (!binding.SourceType.IsAccessible)
{
actualSourceVar = "typedSource";
AppendLine($"var {actualSourceVar} = ({binding.SourceType}){sourceVariableName};");
}

var assignedValueExpression = valueVariableName;

// Cast value if it's an inaccessible type
if (!binding.PropertyType.IsAccessible)
{
var typedValueVar = "typedValue";
AppendLine($"var {typedValueVar} = ({binding.PropertyType}){valueVariableName};");
assignedValueExpression = typedValueVar;
}

// early return for nullable values if the setter doesn't accept them
if (binding.PropertyType.IsNullable && !binding.SetterOptions.AcceptsNullValue)
{
if (binding.PropertyType.IsValueType)
{
AppendLine($"if (!{valueVariableName}.HasValue)");
assignedValueExpression = $"{valueVariableName}.Value";
var checkVar = !binding.PropertyType.IsAccessible ? "typedValue" : valueVariableName;
AppendLine($"if (!{checkVar}.HasValue)");
assignedValueExpression = $"{checkVar}.Value";
}
else
{
AppendLine($"if ({valueVariableName} is null)");
var checkVar = !binding.PropertyType.IsAccessible ? "typedValue" : valueVariableName;
AppendLine($"if ({checkVar} is null)");
}
AppendLine('{');
Indent();
Expand All @@ -286,7 +356,7 @@ private void AppendSetterLambda(BindingInvocationDescription binding, string sou
AppendLine('}');
}

var setter = Setter.From(binding.Path, sourceVariableName, assignedValueExpression);
var setter = Setter.From(binding.Path, actualSourceVar, assignedValueExpression);
if (setter.PatternMatchingExpressions.Length > 0)
{
Append("if (");
Expand Down Expand Up @@ -331,12 +401,20 @@ private void AppendSetterLambda(BindingInvocationDescription binding, string sou

private void AppendHandlersArray(BindingInvocationDescription binding)
{
AppendLine($"new global::System.Tuple<global::System.Func<{binding.SourceType}, object?>, string>[]");
var sourceTypeForHandlers = GetTypeForBinding(binding.SourceType);
AppendLine($"new global::System.Tuple<global::System.Func<{sourceTypeForHandlers}, object?>, string>[]");
AppendLine('{');

Indent();

string nextExpression = "source";
// If source type is inaccessible, we need to cast in the lambda
string sourceExpression = "source";
if (!binding.SourceType.IsAccessible)
{
sourceExpression = $"(({binding.SourceType})source)";
}

string nextExpression = sourceExpression;
bool forceConditonalAccessToNextPart = false;
foreach (var part in binding.Path)
{
Expand Down Expand Up @@ -469,6 +547,13 @@ private void AppendUnsafePropertySetAccessors(string propertyName, string member
static extern void {{CreateUnsafePropertyAccessorSetMethodName(propertyName)}}({{containingType}} source, {{memberType}} value);
""");

private void AppendUnsafeAccessorTypes(BindingInvocationDescription binding)
{
// Note: UnsafeAccessorType is applied to the getter parameter in AppendFunctionArguments
// We don't need additional declarations here since the attribute on the parameter
// is sufficient to enable casting from/to object in the method body
}

public void Dispose()
{
_indentedTextWriter.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public sealed record TypeDescription(
string GlobalName,
bool IsValueType = false,
bool IsNullable = false,
bool IsGenericParameter = false)
bool IsGenericParameter = false,
bool IsAccessible = true,
string? AssemblyQualifiedName = null)
{
public override string ToString()
=> IsNullable
Expand Down
5 changes: 1 addition & 4 deletions src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,7 @@ private static Result<ITypeSymbol> GetLambdaParameterType(LambdaExpressionSyntax
}

var lambdaParamType = parameters[0].Type;
if (!lambdaParamType.IsAccessible())
{
return Result<ITypeSymbol>.Failure(DiagnosticsFactory.UnaccessibleTypeUsedAsLambdaParameter(lambda.GetLocation()));
}
// Now we support inaccessible types using UnsafeAccessorType

return Result<ITypeSymbol>.Success(lambdaParamType);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ public static DiagnosticInfo UnaccessibleTypeUsedAsLambdaParameter(Location loca
=> new DiagnosticInfo(
new DiagnosticDescriptor(
id: "BSG0007",
title: "Unaccessible type used as lambda parameter",
messageFormat: "The lambda parameter type has to be declared as public, internal or protected internal.",
title: "Inaccessible type used as lambda parameter",
messageFormat: "The lambda parameter type has to be declared as public, internal or protected internal. Private and protected types cannot be used because the generated binding code is in a different namespace. Consider making the type internal instead of private.",
category: "Usage",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true),
Expand Down
21 changes: 20 additions & 1 deletion src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ public static bool IsTypeNullable(this ITypeSymbol typeInfo, bool enabledNullabl
public static TypeDescription CreateTypeDescription(this ITypeSymbol typeSymbol, bool enabledNullable)
{
var isNullable = IsTypeNullable(typeSymbol, enabledNullable);
var isAccessible = typeSymbol.IsAccessible();
return new TypeDescription(
GlobalName: GetGlobalName(typeSymbol, isNullable, typeSymbol.IsValueType),
IsNullable: isNullable,
IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, //TODO: Add support for generic parameters
IsValueType: typeSymbol.IsValueType);
IsValueType: typeSymbol.IsValueType,
IsAccessible: isAccessible,
AssemblyQualifiedName: isAccessible ? null : GetAssemblyQualifiedName(typeSymbol));
}

private static bool IsNullableValueType(this ITypeSymbol typeInfo) =>
Expand All @@ -43,4 +46,20 @@ private static string GetGlobalName(this ITypeSymbol typeSymbol, bool isNullable

return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}

private static string GetAssemblyQualifiedName(this ITypeSymbol typeSymbol)
{
// For UnsafeAccessorType, we need assembly-qualified name
// Format: "FullTypeName, AssemblyName"
var typeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.Replace("global::", ""); // Remove global:: prefix

var containingAssembly = typeSymbol.ContainingAssembly;
if (containingAssembly != null)
{
return $"{typeName}, {containingAssembly.Name}";
}

return typeName;
}
}
Loading