Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Binding source generator #21725

Merged
merged 47 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7b61154
Add new public API SetBinding<TSource, TProperty>
simonrozsival Mar 4, 2024
632979b
Add source generator skeleton
simonrozsival Mar 5, 2024
403f476
Setup unit tests for binding intermediate representation
jkurdek Mar 26, 2024
f9747a3
Added basic nullability support
jkurdek Mar 29, 2024
a4b9335
Remove unnecessary Id from the binding record
simonrozsival Apr 2, 2024
62f613e
Generate casts
simonrozsival Apr 2, 2024
e5343a5
Split index and member access
simonrozsival Apr 3, 2024
5d719ca
Cleanup
simonrozsival Apr 3, 2024
dd8112f
Update test case
simonrozsival Apr 3, 2024
6b325ae
Cleanup
simonrozsival Apr 4, 2024
e9093af
Fix the semantic of conditional access
simonrozsival Apr 5, 2024
5298785
add as-cast suport to source generator
jkurdek Apr 4, 2024
d1fed61
improve nullability check
jkurdek Apr 4, 2024
c8130cf
specify params in tests only when not default
jkurdek Apr 4, 2024
c0b7da6
simplify diagnostics
jkurdek Apr 4, 2024
7e82570
move path parser to separate class
jkurdek Apr 4, 2024
7f3f2bb
small cleanup
jkurdek Apr 4, 2024
491a91e
Get nullability right in binding representation tests
jkurdek Apr 5, 2024
1bf4cad
Fix path parse to work with improved tests
jkurdek Apr 5, 2024
0355390
Integration tests (#14)
jkurdek Apr 8, 2024
d00f0d8
Implement setters (#16)
simonrozsival Apr 9, 2024
b4bf3e0
Simplify indexes (#18)
simonrozsival Apr 9, 2024
97721da
Fix overload check (#20)
jkurdek Apr 9, 2024
8617d5b
Implement detection of writable properties (#19)
simonrozsival Apr 9, 2024
1f733e2
Add TODOs to change arrays to equatable arrays
simonrozsival Apr 9, 2024
b375a60
Add projects to solutions
simonrozsival Apr 9, 2024
85d0a40
Try to run unit tests in CI
simonrozsival Apr 10, 2024
8708c17
Added custom indexer support
jkurdek Apr 11, 2024
78502ca
Fix typo
simonrozsival Apr 16, 2024
d2befa0
Avoid allocating line separators array
simonrozsival Apr 16, 2024
2039bb0
Hide the new SetBinding overload from editor suggestions for now
simonrozsival Apr 17, 2024
08f55d6
Incremental generation tests
jkurdek Apr 16, 2024
89dfa15
replaced array with equatable array
jkurdek Apr 19, 2024
df57581
Added source information + formatting
jkurdek Apr 19, 2024
6d6887e
added third party licenses
jkurdek Apr 19, 2024
4cb21c1
Add benchmark for source-generated SetBinding (#25)
simonrozsival Apr 23, 2024
19b6885
Improve diagnostic messages
simonrozsival Apr 23, 2024
f02cce5
Improved incrementality testing (#28)
jkurdek Apr 29, 2024
2f35e7e
Add C-Style casts support (#26)
jkurdek Apr 30, 2024
2cb89e8
Cleanup (#29)
simonrozsival Apr 30, 2024
0835449
Improved nullable support (#30)
jkurdek May 14, 2024
55e1aa2
Simplify building setter
simonrozsival May 14, 2024
44c0167
cleaned up methods in BindingSourceGeneratorUtilities
jkurdek May 17, 2024
296c5e0
replaced tuples with Result<T>
jkurdek May 17, 2024
1054597
simplified naming
jkurdek May 17, 2024
1f88a39
Fix and improve unit test project
simonrozsival Jun 4, 2024
afabedc
Fix bad conflict resolution in solution file
simonrozsival Jun 4, 2024
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
Prev Previous commit
Next Next commit
Incremental generation tests
  • Loading branch information
jkurdek authored and simonrozsival committed Jun 4, 2024
commit 08f55d6c7dfb5feb37c1bbaa185a0d84603dece1
2 changes: 1 addition & 1 deletion src/Controls/src/BindingSourceGen/BindingCodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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})]");
}
Expand Down
61 changes: 43 additions & 18 deletions src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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();


Expand All @@ -57,18 +58,18 @@ static bool IsSetBindingMethod(SyntaxNode node)

static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t)
{
var diagnostics = new List<Diagnostic>();
var diagnostics = new List<DiagnosticInfo>();
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);

Expand Down Expand Up @@ -98,15 +99,15 @@ 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(),
SetterOptions: DeriveSetterOptions(lambdaBody, context.SemanticModel, enabledNullable));
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)
Expand All @@ -120,10 +121,10 @@ private static Diagnostic[] VerifyCorrectOverload(InvocationExpressionSyntax inv
return [DiagnosticsFactory.SuboptimalSetBindingOverload(getter.GetLocation())];
}

return Array.Empty<Diagnostic>();
return Array.Empty<DiagnosticInfo>();
}

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;
Expand All @@ -138,7 +139,7 @@ private static (ExpressionSyntax? lambdaBodyExpression, IMethodSymbol? lambdaSym
return (null, null, [DiagnosticsFactory.GetterIsNotLambda(lambda.GetLocation())]);
}

return (lambdaBody, lambdaSymbol, Array.Empty<Diagnostic>());
return (lambdaBody, lambdaSymbol, Array.Empty<DiagnosticInfo>());
}

private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExpression, SemanticModel semanticModel, bool enabledNullable)
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 20 additions & 8 deletions src/Controls/src/BindingSourceGen/DiagnosticsFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
113 changes: 113 additions & 0 deletions src/Controls/src/BindingSourceGen/EquatableArray.cs
Original file line number Diff line number Diff line change
@@ -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<T> AsEquatableArray<T>(this T[] array)
where T : IEquatable<T>
{
return new(array);
}
}

public readonly struct EquatableArray<T> : IEquatable<EquatableArray<T>>, IEnumerable<T>
where T : IEquatable<T>
{
private readonly T[]? array;

private EquatableArray(ImmutableArray<T> array)
{
this.array = Unsafe.As<ImmutableArray<T>, 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<T> array)
{
return AsSpan().SequenceEqual(array.AsSpan());
}

public override bool Equals(object? obj)
{
return obj is EquatableArray<T> 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<T> AsImmutableArray()
{
return Unsafe.As<T[]?, ImmutableArray<T>>(ref Unsafe.AsRef(in this.array));
}

public static EquatableArray<T> FromImmutableArray(ImmutableArray<T> array)
{
return new(array);
}

public ReadOnlySpan<T> AsSpan()
{
return AsImmutableArray().AsSpan();
}

public T[] ToArray()
{
return AsImmutableArray().ToArray();
}

public ImmutableArray<T>.Enumerator GetEnumerator()
{
return AsImmutableArray().GetEnumerator();
}

IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return ((IEnumerable<T>)AsImmutableArray()).GetEnumerator();
}

IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}

public static bool operator ==(EquatableArray<T> left, EquatableArray<T> right)
{
return left.Equals(right);
}

public static bool operator !=(EquatableArray<T> left, EquatableArray<T> right)
{
return !left.Equals(right);
}
}
Loading