File: BindingCodeWriter.cs
Web Access
Project: src\src\Controls\src\BindingSourceGen\Controls.BindingSourceGen.csproj (Microsoft.Maui.Controls.BindingSourceGen)
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--;
	}
}