|
using System.CodeDom.Compiler;
using System.Globalization;
using System.Runtime.InteropServices;
using static Microsoft.Maui.Controls.BindingSourceGen.UnsafeAccessorsMethodName;
namespace Microsoft.Maui.Controls.BindingSourceGen;
public static class BindingCodeWriter
{
private static readonly string NewLine = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "\r\n" : "\n";
public static string GeneratedCodeAttribute => $"[GeneratedCodeAttribute(\"{typeof(BindingCodeWriter).Assembly.FullName}\", \"{typeof(BindingCodeWriter).Assembly.GetName().Version}\")]";
public static string GenerateCommonCode() => $$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a .NET MAUI source generator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable
namespace Microsoft.Maui.Controls.Generated
{
using System.CodeDom.Compiler;
{{GeneratedCodeAttribute}}
internal static partial class GeneratedBindingInterceptors
{
private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableProperty)
=> mode == BindingMode.OneWayToSource
|| mode == BindingMode.TwoWay
|| (mode == BindingMode.Default
&& (bindableProperty.DefaultBindingMode == BindingMode.OneWayToSource
|| bindableProperty.DefaultBindingMode == BindingMode.TwoWay));
private static bool ShouldUseSetter(BindingMode mode)
=> mode == BindingMode.OneWayToSource
|| mode == BindingMode.TwoWay
|| mode == BindingMode.Default;
}
}
""";
private static string GenerateUnsafeFieldAccessor(string fieldName, string memberType, string containingType, uint id) => $$"""
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "{{fieldName}}")]
private static extern ref {{memberType}} {{CreateUnsafeFieldAccessorMethodName(id, fieldName)}}({{containingType}} source);
""";
private static string GenerateUnsafePropertyAccessors(string propertyName, string memberType, string containingType, uint id, bool generateSetter)
{
var propertyAccessors = new List<string>
{
GenerateUnsafePropertyGetAccessors(propertyName, memberType, containingType, id)
};
if (generateSetter)
{
propertyAccessors.Add(GenerateUnsafePropertySetAccessors(propertyName, memberType, containingType, id));
}
return string.Join(NewLine, propertyAccessors);
}
private static string GenerateUnsafePropertyGetAccessors(string propertyName, string memberType, string containingType, uint id) => $$"""
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_{{propertyName}}")]
private static extern {{memberType}} {{CreateUnsafePropertyAccessorGetMethodName(id, propertyName)}}({{containingType}} source);
""";
private static string GenerateUnsafePropertySetAccessors(string propertyName, string memberType, string containingType, uint id) => $$"""
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_{{propertyName}}")]
private static extern void {{CreateUnsafePropertyAccessorSetMethodName(id, propertyName)}}({{containingType}} source, {{memberType}} value);
""";
private static string GenerateBindingCode(string bindingMethodBody, IEnumerable<string> unsafeAccessors) => $$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a .NET MAUI source generator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable
namespace System.Runtime.CompilerServices
{
using System;
using System.CodeDom.Compiler;
{{GeneratedCodeAttribute}}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : Attribute
{
public InterceptsLocationAttribute(string filePath, int line, int column)
{
FilePath = filePath;
Line = line;
Column = column;
}
public string FilePath { get; }
public int Line { get; }
public int Column { get; }
}
}
namespace Microsoft.Maui.Controls.Generated
{
using System;
using System.CodeDom.Compiler;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Controls.Internals;
internal static partial class GeneratedBindingInterceptors
{
{{bindingMethodBody}}
{{string.Join(NewLine, unsafeAccessors)}}
}
}
""";
public static string GenerateBinding(BindingInvocationDescription binding, uint id)
{
if (!binding.NullableContextEnabled)
{
var referenceTypesConditionalAccessTransformer = new ReferenceTypesConditionalAccessTransformer();
binding = referenceTypesConditionalAccessTransformer.Transform(binding);
}
var unsafeAccessors = binding.Path
.OfType<InaccessibleMemberAccess>();
var bindingMethod = GenerateBindingMethod(binding, id);
var unsafeAccessorsStrings = new List<string>();
foreach (var unsafeAccessor in unsafeAccessors)
{
var accessor = unsafeAccessor.Kind switch
{
AccessorKind.Field => GenerateUnsafeFieldAccessor(unsafeAccessor.MemberName, unsafeAccessor.memberType.GlobalName, unsafeAccessor.ContainingType.GlobalName, id),
AccessorKind.Property => GenerateUnsafePropertyAccessors(unsafeAccessor.MemberName, unsafeAccessor.memberType.GlobalName, unsafeAccessor.ContainingType.GlobalName, id, binding.SetterOptions.IsWritable),
_ => throw new ArgumentException(nameof(unsafeAccessor.Kind))
};
unsafeAccessorsStrings.Add(accessor);
}
return GenerateBindingCode(bindingMethod, unsafeAccessorsStrings);
}
private static string GenerateBindingMethod(BindingInvocationDescription binding, uint id)
{
using var builder = new BindingInterceptorCodeBuilder(indent: 2);
builder.AppendSetBindingInterceptor(id: id, binding: binding);
return builder.ToString();
}
public sealed class BindingInterceptorCodeBuilder : IDisposable
{
private StringWriter _stringWriter;
private IndentedTextWriter _indentedTextWriter;
public override string ToString()
{
_indentedTextWriter.Flush();
return _stringWriter.ToString();
}
public BindingInterceptorCodeBuilder(int indent = 0)
{
_stringWriter = new StringWriter(CultureInfo.InvariantCulture);
_indentedTextWriter = new IndentedTextWriter(_stringWriter, "\t") { Indent = indent };
}
public void AppendSetBindingInterceptor(uint id, BindingInvocationDescription binding)
{
AppendLine(GeneratedCodeAttribute);
AppendInterceptorAttribute(binding.Location);
AppendMethodName(binding, id);
if (binding.SourceType.IsGenericParameter && binding.PropertyType.IsGenericParameter)
{
Append($"<{binding.SourceType}, {binding.PropertyType}>");
}
else if (binding.SourceType.IsGenericParameter)
{
Append($"<{binding.SourceType}>");
}
else if (binding.PropertyType.IsGenericParameter)
{
Append($"<{binding.PropertyType}>");
}
AppendLine('(');
AppendFunctionArguments(binding);
AppendLines($$"""
{
Action<{{binding.SourceType}}, {{binding.PropertyType}}>? setter = null;
if ({{GetShouldUseSetterCall(binding.MethodType)}})
{
""");
Indent();
Indent();
if (binding.SetterOptions.IsWritable)
{
AppendLines("""
setter = static (source, value) =>
{
""");
Indent();
AppendSetterAction(binding, id);
Unindent();
AppendLine("};");
}
else
{
AppendLine("throw new InvalidOperationException(\"Cannot set value on the source object.\");");
}
Unindent();
Unindent();
AppendLines($$"""
}
var binding = new TypedBinding<{{binding.SourceType}}, {{binding.PropertyType}}>(
getter: source => (getter(source), true),
setter,
""");
Indent();
Indent();
Append("handlers: ");
AppendHandlersArray(binding, id);
AppendLine(")");
Unindent();
Unindent();
AppendLines($$"""
{
Mode = mode,
Converter = converter,
ConverterParameter = converterParameter,
StringFormat = stringFormat,
Source = source,
FallbackValue = fallbackValue,
TargetNullValue = targetNullValue
};
{{GetEpilog(binding.MethodType)}}
}
""");
}
private void AppendFunctionArguments(BindingInvocationDescription binding)
{
if (binding.MethodType == InterceptedMethodType.SetBinding)
{
AppendLines($$"""
this BindableObject bindableObject,
BindableProperty bindableProperty,
""");
}
AppendLines($$"""
Func<{{binding.SourceType}}, {{binding.PropertyType}}> getter,
BindingMode mode = BindingMode.Default,
IValueConverter? converter = null,
object? converterParameter = null,
string? stringFormat = null,
object? source = null,
object? fallbackValue = null,
object? targetNullValue = null)
""");
}
private static string GetEpilog(InterceptedMethodType interceptedMethodType) =>
interceptedMethodType switch
{
InterceptedMethodType.SetBinding => "bindableObject.SetBinding(bindableProperty, binding);",
InterceptedMethodType.Create => "return binding;",
_ => throw new ArgumentOutOfRangeException(nameof(interceptedMethodType))
};
private static string GetShouldUseSetterCall(InterceptedMethodType interceptedMethodType) =>
interceptedMethodType switch
{
InterceptedMethodType.SetBinding => "ShouldUseSetter(mode, bindableProperty)",
InterceptedMethodType.Create => "ShouldUseSetter(mode)",
_ => throw new ArgumentOutOfRangeException(nameof(interceptedMethodType))
};
private void AppendMethodName(BindingInvocationDescription binding, uint id)
{
Append(binding.MethodType switch
{
InterceptedMethodType.SetBinding => $"public static void SetBinding{id}",
InterceptedMethodType.Create => $"public static BindingBase Create{id}",
_ => throw new ArgumentOutOfRangeException(nameof(binding.MethodType))
});
}
private void AppendInterceptorAttribute(InterceptorLocation location)
{
AppendLine($"[InterceptsLocationAttribute(@\"{location.FilePath}\", {location.Line}, {location.Column})]");
}
private void AppendSetterAction(BindingInvocationDescription binding, uint id, string sourceVariableName = "source", string valueVariableName = "value")
{
var assignedValueExpression = valueVariableName;
// 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";
}
else
{
AppendLine($"if ({valueVariableName} is null)");
}
AppendLine('{');
Indent();
AppendLine("return;");
Unindent();
AppendLine('}');
}
var setter = Setter.From(binding.Path, id, sourceVariableName, assignedValueExpression);
if (setter.PatternMatchingExpressions.Length > 0)
{
Append("if (");
for (int i = 0; i < setter.PatternMatchingExpressions.Length; i++)
{
if (i == 1)
{
Indent();
}
if (i > 0)
{
AppendBlankLine();
Append("&& ");
}
Append(setter.PatternMatchingExpressions[i]);
}
AppendLine(')');
if (setter.PatternMatchingExpressions.Length > 1)
{
Unindent();
}
AppendLine('{');
Indent();
}
AppendLine(setter.AssignmentStatement);
if (setter.PatternMatchingExpressions.Length > 0)
{
Unindent();
AppendLine('}');
}
}
private void AppendHandlersArray(BindingInvocationDescription binding, uint id)
{
AppendLine($"new Tuple<Func<{binding.SourceType}, object?>, string>[]");
AppendLine('{');
Indent();
string nextExpression = "source";
bool forceConditonalAccessToNextPart = false;
foreach (var part in binding.Path)
{
var previousExpression = nextExpression;
nextExpression = AccessExpressionBuilder.ExtendExpression(previousExpression, MaybeWrapInConditionalAccess(part, forceConditonalAccessToNextPart), id);
forceConditonalAccessToNextPart = part is Cast;
// Make binding react for PropertyChanged events on indexer itself
if (part is IndexAccess indexAccess)
{
AppendLine($"new(static source => {previousExpression}, \"{indexAccess.DefaultMemberName}\"),");
}
else if (part is ConditionalAccess conditionalAccess && conditionalAccess.Part is IndexAccess innerIndexAccess)
{
AppendLine($"new(static source => {previousExpression}, \"{innerIndexAccess.DefaultMemberName}\"),");
}
// Some parts don't have a property name, so we can't generate a handler for them (for example casts)
if (part.PropertyName is string propertyName)
{
AppendLine($"new(static source => {previousExpression}, \"{propertyName}\"),");
}
}
Unindent();
Append('}');
static IPathPart MaybeWrapInConditionalAccess(IPathPart part, bool forceConditonalAccess)
{
if (!forceConditonalAccess)
{
return part;
}
return part switch
{
MemberAccess memberAccess => new ConditionalAccess(memberAccess),
IndexAccess indexAccess => new ConditionalAccess(indexAccess),
_ => part,
};
}
}
public void Dispose()
{
_indentedTextWriter.Dispose();
_stringWriter.Dispose();
}
private void AppendBlankLine() => _indentedTextWriter.WriteLine();
private void AppendLine(string line) => _indentedTextWriter.WriteLine(line);
private void AppendLine(char character) => _indentedTextWriter.WriteLine(character);
private void Append(string str) => _indentedTextWriter.Write(str);
private void Append(char character) => _indentedTextWriter.Write(character);
private readonly char[] LineSeparators = ['\n', '\r'];
private void AppendLines(string lines)
{
foreach (var line in lines.Split(LineSeparators, StringSplitOptions.RemoveEmptyEntries))
{
AppendLine(line);
}
}
private void Indent() => _indentedTextWriter.Indent++;
private void Unindent() => _indentedTextWriter.Indent--;
}
}
|