-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add support for private types in SetBinding using UnsafeAccessorType #32618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
fc949b4
2af172b
e45d888
d17f37e
197b10a
f3c1143
9969718
231925e
9ce5965
9ac3e27
d666f5c
6fe246a
86c933f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)}}) | ||
| { | ||
| """); | ||
|
|
@@ -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(); | ||
|
|
@@ -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('('); | ||
|
|
@@ -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, | ||
|
|
@@ -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 | ||
| { | ||
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
|
@@ -415,32 +453,38 @@ private void AppendBindingPropertySetters(BindingPropertyFlags propertyFlags) | |
| private void AppendUnsafeAccessors(BindingInvocationDescription binding) | ||
| { | ||
| // Append unsafe accessors as local methods | ||
| var unsafeAccessors = binding.Path.OfType<InaccessibleMemberAccess>(); | ||
| // Need to unwrap ConditionalAccess to find InaccessibleMemberAccess parts | ||
| var unsafeAccessors = binding.Path | ||
| .Select(part => part is ConditionalAccess ca ? ca.Part : part) | ||
| .OfType<InaccessibleMemberAccess>(); | ||
|
|
||
| foreach (var unsafeAccessor in unsafeAccessors) | ||
| { | ||
| AppendBlankLine(); | ||
|
|
||
| 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) | ||
| { | ||
| bool isLastPart = unsafeAccessor.Equals(binding.Path.Last()); | ||
| // Check if this is the last part, unwrapping ConditionalAccess if needed | ||
| var lastPart = binding.Path.Last(); | ||
| var unwrappedLast = lastPart is ConditionalAccess ca ? ca.Part : lastPart; | ||
| bool isLastPart = unsafeAccessor.Equals(unwrappedLast); | ||
| bool needsGetterForLastPart = binding.RequiresAllUnsafeGetters; | ||
|
|
||
| if (!isLastPart || needsGetterForLastPart) | ||
| { | ||
| // 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 | ||
|
|
@@ -469,6 +513,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(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot is the
objectsometimes supposed to beobject?? or is it always non-nullable (the?might be added later?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 6fe246a. GetTypeForSignature now returns
object?when the type is both inaccessible and nullable, otherwiseobject.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot add snapshot bindingsourcegen tests which would show:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 86c933f. The three new snapshot tests demonstrate:
Func<object?, string>andTypedBinding<object?, string>object?in UnsafeAccessor parameterobject?for UnsafeAccessor parameters and return typesAll tests verify the correct generation of
object?for nullable inaccessible types throughout signatures, TypedBinding instantiation, and UnsafeAccessor methods.