Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 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
112 changes: 103 additions & 9 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,25 @@ 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)
{
// When types are inaccessible, the getter parameter is Func<object, object> (or mixed)
// and we just invoke it directly - no casting needed because the parameter types match
return "getter(source), true";
}

private void AppendFunctionArguments(BindingInvocationDescription binding)
{
AppendLine('(');
Expand All @@ -218,8 +236,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 +259,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,6 +298,8 @@ private void AppendSetterLambda(BindingInvocationDescription binding, string sou
AppendLine('{');
Indent();

// No casting needed - when types are inaccessible, parameters are already object
// and UnsafeAccessor methods handle the type conversions
var assignedValueExpression = valueVariableName;

// early return for nullable values if the setter doesn't accept them
Expand Down Expand Up @@ -331,11 +366,14 @@ 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();

// No casting needed - when source type is inaccessible, it's already object
// and we use UnsafeAccessor methods for member access
string nextExpression = "source";
bool forceConditonalAccessToNextPart = false;
foreach (var part in binding.Path)
Expand Down Expand Up @@ -423,7 +461,7 @@ private void AppendUnsafeAccessors(BindingInvocationDescription binding)

if (unsafeAccessor.Kind == AccessorKind.Field)
{
AppendUnsafeFieldAccessor(unsafeAccessor.MemberName, unsafeAccessor.memberType.GlobalName, unsafeAccessor.ContainingType.GlobalName);
AppendUnsafeFieldAccessorWithType(unsafeAccessor.MemberName, unsafeAccessor.memberType, unsafeAccessor.ContainingType);
}
else if (unsafeAccessor.Kind == AccessorKind.Property)
{
Expand All @@ -434,13 +472,13 @@ private void AppendUnsafeAccessors(BindingInvocationDescription binding)
{
// we don't need the unsafe getter if the item is the very last part of the path
// because we don't need to access its value while constructing the handlers array
AppendUnsafePropertyGetAccessors(unsafeAccessor.MemberName, unsafeAccessor.memberType.GlobalName, unsafeAccessor.ContainingType.GlobalName);
AppendUnsafePropertyGetAccessorsWithType(unsafeAccessor.MemberName, unsafeAccessor.memberType, unsafeAccessor.ContainingType);
}

if (isLastPart && binding.SetterOptions.IsWritable)
{
// We only need the unsafe setter if the item is the very last part of the path
AppendUnsafePropertySetAccessors(unsafeAccessor.MemberName, unsafeAccessor.memberType.GlobalName, unsafeAccessor.ContainingType.GlobalName);
AppendUnsafePropertySetAccessorsWithType(unsafeAccessor.MemberName, unsafeAccessor.memberType, unsafeAccessor.ContainingType);
}
}
else
Expand Down Expand Up @@ -469,6 +507,62 @@ private void AppendUnsafePropertySetAccessors(string propertyName, string member
static extern void {{CreateUnsafePropertyAccessorSetMethodName(propertyName)}}({{containingType}} source, {{memberType}} value);
""");

private void AppendUnsafeFieldAccessorWithType(string fieldName, TypeDescription memberType, TypeDescription containingType)
{
var memberTypeStr = memberType.IsAccessible ? memberType.ToString() : "object";
var containingTypeStr = containingType.IsAccessible ? containingType.ToString() : "object";

if (!containingType.IsAccessible && containingType.AssemblyQualifiedName != null)
{
AppendLine($"[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = \"{fieldName}\")]");
AppendLine($"static extern ref {memberTypeStr} {CreateUnsafeFieldAccessorMethodName(fieldName)}([global::System.Runtime.CompilerServices.UnsafeAccessorType(\"{containingType.AssemblyQualifiedName}\")] {containingTypeStr} source);");
}
else
{
AppendUnsafeFieldAccessor(fieldName, memberTypeStr, containingTypeStr);
}
}

private void AppendUnsafePropertyGetAccessorsWithType(string propertyName, TypeDescription memberType, TypeDescription containingType)
{
var memberTypeStr = memberType.IsAccessible ? memberType.ToString() : "object";
var containingTypeStr = containingType.IsAccessible ? containingType.ToString() : "object";

if (!containingType.IsAccessible && containingType.AssemblyQualifiedName != null)
{
AppendLine($"[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = \"get_{propertyName}\")]");
AppendLine($"static extern {memberTypeStr} {CreateUnsafePropertyAccessorGetMethodName(propertyName)}([global::System.Runtime.CompilerServices.UnsafeAccessorType(\"{containingType.AssemblyQualifiedName}\")] {containingTypeStr} source);");
}
else
{
AppendUnsafePropertyGetAccessors(propertyName, memberTypeStr, containingTypeStr);
}
}

private void AppendUnsafePropertySetAccessorsWithType(string propertyName, TypeDescription memberType, TypeDescription containingType)
{
var memberTypeStr = memberType.IsAccessible ? memberType.ToString() : "object";
var containingTypeStr = containingType.IsAccessible ? containingType.ToString() : "object";
var valueTypeStr = memberType.IsAccessible ? memberType.ToString() : "object";

if (!containingType.IsAccessible && containingType.AssemblyQualifiedName != null)
{
AppendLine($"[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = \"set_{propertyName}\")]");
AppendLine($"static extern void {CreateUnsafePropertyAccessorSetMethodName(propertyName)}([global::System.Runtime.CompilerServices.UnsafeAccessorType(\"{containingType.AssemblyQualifiedName}\")] {containingTypeStr} source, {valueTypeStr} value);");
}
else
{
AppendUnsafePropertySetAccessors(propertyName, valueTypeStr, containingTypeStr);
}
}

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
19 changes: 19 additions & 0 deletions src/Controls/src/BindingSourceGen/ISymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ internal static bool IsAccessible(this ISymbol symbol) =>
|| symbol.DeclaredAccessibility == Accessibility.Internal
|| symbol.DeclaredAccessibility == Accessibility.ProtectedOrInternal;

// For type symbols, check if the type and all its containing types are accessible
internal static bool IsAccessible(this ITypeSymbol typeSymbol)
{
// Check the type itself
if (!((ISymbol)typeSymbol).IsAccessible())
return false;

// Check all containing types
var containingType = typeSymbol.ContainingType;
while (containingType != null)
{
if (!((ISymbol)containingType).IsAccessible())
return false;
containingType = containingType.ContainingType;
}

return true;
}

internal static AccessorKind ToAccessorKind(this ISymbol symbol)
{
return symbol switch
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;
}
}
10 changes: 7 additions & 3 deletions src/Controls/src/BindingSourceGen/PathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ private Result<List<IPathPart>> HandleMemberAccessExpression(MemberAccessExpress
var memberType = typeInfo.CreateTypeDescription(_enabledNullable);
var containgType = symbol.ContainingType.CreateTypeDescription(_enabledNullable);

IPathPart part = symbol.IsAccessible()
? new MemberAccess(member, !isReferenceType)
: new InaccessibleMemberAccess(containgType, memberType, accessorKind, member, !isReferenceType);
// If either the member is inaccessible OR the containing type is inaccessible,
// we need to use UnsafeAccessor
bool needsUnsafeAccessor = !symbol.IsAccessible() || !containgType.IsAccessible;

IPathPart part = needsUnsafeAccessor
? new InaccessibleMemberAccess(containgType, memberType, accessorKind, member, !isReferenceType)
: new MemberAccess(member, !isReferenceType);

result.Value.Add(part);
return Result<List<IPathPart>>.Success(result.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,10 @@ public void Foo()
[InlineData("protected")]
[InlineData("private protected")]
// https://github.com/dotnet/maui/issues/23534
public void ReportsWarningWhenSourceTypeIsUnaccessible(string modifier)
public void SupportsInaccessibleSourceType(string modifier)
{
// Previously this test checked for BSG0007 error
// Now we support private types using UnsafeAccessorType
var source = $$"""
using Microsoft.Maui.Controls;

Expand All @@ -255,9 +257,9 @@ public void Bar()

var result = SourceGenHelpers.Run(source);

var diagnostic = Assert.Single(result.SourceGeneratorDiagnostics);
Assert.Equal("BSG0007", diagnostic.Id);
AssertExtensions.AssertNoDiagnostics(result.GeneratedCodeCompilationDiagnostics, "Generated code compilation");
// Should not have any diagnostics - private types are now supported
AssertExtensions.AssertNoDiagnostics(result);
Assert.NotNull(result.Binding);
}

[Fact]
Expand Down
Loading