Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -106,15 +109,25 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext
private static EquatableArray<DiagnosticInfo> VerifyCorrectOverload(InvocationExpressionSyntax invocation, GeneratorSyntaxContext context, CancellationToken t)
{
var argumentList = invocation.ArgumentList.Arguments;

if (argumentList.Count < 2)
{
return new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.SuboptimalSetBindingOverload(invocation.GetLocation())]);
throw new ArgumentOutOfRangeException(nameof(invocation));
}

var getter = argumentList[1].Expression;
if (getter is not LambdaExpressionSyntax)
var secondArgument = argumentList[1].Expression;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we do the validation of the number of arguments in IsSetBindingMethod but it's still making me a little uneasy to just reach for an index without a check for the length of the array in the same scope. This could break if somebody refactors this code later. I'd just add ArgumentOutOfRangeException.ThrowIfLessThan(argumentList, 2);


if (secondArgument is IdentifierNameSyntax)
{
return new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.SuboptimalSetBindingOverload(getter.GetLocation())]);
var type = context.SemanticModel.GetTypeInfo(secondArgument, cancellationToken: t).Type;
if (type != null && type.Name == "Func")
{
return new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.GetterIsNotLambda(secondArgument.GetLocation())]);
}
else // String and Binding
{
return new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.SuboptimalSetBindingOverload(secondArgument.GetLocation())]);
}
}

return [];
Expand Down
94 changes: 47 additions & 47 deletions src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
126 changes: 86 additions & 40 deletions src/Controls/tests/BindingSourceGen.UnitTests/DiagnosticsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,103 @@ 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();
var getter = new Func<Button, int>(b => b.Text.Length);
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;
Expand All @@ -63,26 +109,26 @@ 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();

var array = new int[1, 1];
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);
}
}
Original file line number Diff line number Diff line change
@@ -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<MyObject> Children { get; private set; } = new List<MyObject>();
}

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);
}
}
}
}
Loading