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]);