Skip to content

Commit

Permalink
Merge pull request dotnet#6 from jkurdek/binding-source-generator
Browse files Browse the repository at this point in the history
setup unit tests for binding intermediate representation
  • Loading branch information
simonrozsival committed Mar 26, 2024
2 parents 9a3ee24 + 8ac1808 commit 2286ee4
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 58 deletions.
207 changes: 156 additions & 51 deletions src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs
@@ -1,68 +1,173 @@
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)
)
.Where(static binding => binding != null)
.Select(static (binding, t) => binding!)
.WithTrackingName("BindingsWithDiagnostics")
.Collect();

context.RegisterSourceOutput(bindingsWithDiagnostics, (context, endpoint) =>
context.RegisterSourceOutput(bindingsWithDiagnostics, (spc, bindings) =>
{
foreach (var diagnostic in endpoint.Diagnostics)
{
context.ReportDiagnostic(diagnostic);
}
});

var endpoints = bindingsWithDiagnostics
.Where(endpoint => endpoint.Diagnostics.Count == 0)
.WithTrackingName(GeneratorSteps.EndpointsWithoutDiagnosicsStep);
var codeWriter = new BindingCodeWriter();
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());
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";
}
}

public sealed
static bool IsNullable(ITypeSymbol type)
{
if (type.IsValueType)
{
return false; // TODO: Fix
}
if (type.NullableAnnotation == NullableAnnotation.Annotated)
{
return true;
}
return false;
}

static Binding? GetBindingForGeneration(GeneratorSyntaxContext context, CancellationToken t)
{
var invocation = (InvocationExpressionSyntax)context.Node;

var method = invocation.Expression as MemberAccessExpressionSyntax;


var sourceCodeLocation = new SourceCodeLocation(
context.Node.SyntaxTree.FilePath,
method!.Name.GetLocation().GetLineSpan().StartLinePosition.Line + 1,
method!.Name.GetLocation().GetLineSpan().StartLinePosition.Character + 1
);

var argumentList = invocation.ArgumentList.Arguments;
var getter = argumentList[1].Expression;


if (getter is not LambdaExpressionSyntax lambda)
{
return null; // TODO: Optimize
}


if (context.SemanticModel.GetSymbolInfo(lambda).Symbol is not IMethodSymbol symbol)
{
return null;
}; // TODO

var inputType = symbol.Parameters[0].Type;
var inputTypeGlobalPath = inputType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

var outputType = symbol.ReturnType;
var outputTypeGlobalPath = symbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

var inputTypeIsGenericParameter = inputType.Kind == SymbolKind.TypeParameter;
var outputTypeIsGenericParameter = outputType.Kind == SymbolKind.TypeParameter;

var parts = new List<PathPart>();
ParsePath(lambda.Body, context, parts);

return new Binding(
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
);
}

static void ParsePath(CSharpSyntaxNode? expressionSyntax, GeneratorSyntaxContext context, List<PathPart> parts)
{
if (expressionSyntax is IdentifierNameSyntax identifier)
{
var member = identifier.Identifier.Text;
var typeInfo = context.SemanticModel.GetTypeInfo(identifier).Type;
if (typeInfo == null)
{
return;
}; // TODO
var isNullable = IsNullable(typeInfo);
parts.Add(new PathPart(member, isNullable));
}
else if (expressionSyntax is MemberAccessExpressionSyntax memberAccess)
{
var member = memberAccess.Name.Identifier.Text;
var typeInfo = context.SemanticModel.GetTypeInfo(memberAccess.Expression).Type;
if (typeInfo == null)
{
return;
};
ParsePath(memberAccess.Expression, context, parts); //TODO: Nullable
parts.Add(new PathPart(member, false));
return;
}
else if (expressionSyntax is ElementAccessExpressionSyntax elementAccess)
{
var member = elementAccess.Expression.ToString();
var typeInfo = context.SemanticModel.GetTypeInfo(elementAccess.Expression).Type;
if (typeInfo == null)
{
return;
}; // TODO
parts.Add(new PathPart(member, false, elementAccess.ArgumentList.Arguments[0].Expression)); //TODO: Nullable
ParsePath(elementAccess.Expression, context, parts);
}
else if (expressionSyntax is ConditionalAccessExpressionSyntax conditionalAccess)
{
ParsePath(conditionalAccess.Expression, context, parts);
ParsePath(conditionalAccess.WhenNotNull, context, parts);
return;
}
else if (expressionSyntax is MemberBindingExpressionSyntax memberBinding)
{
var member = memberBinding.Name.Identifier.Text;
parts.Add(new PathPart(member, false)); //TODO: Nullable
return;
}
else if (expressionSyntax is ParenthesizedExpressionSyntax parenthesized)
{
ParsePath(parenthesized.Expression, context, parts);
return;
}
else
{
return;
}
}
}

public sealed record Binding(
int Id,
Expand All @@ -84,17 +189,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(),
};
}
@@ -0,0 +1,71 @@
using Microsoft.Maui.Controls.BindingSourceGen;
using Xunit;
using Binding = Microsoft.Maui.Controls.BindingSourceGen.Binding;


namespace BindingSourceGen.UnitTests;


public class BindingRepresentationGenTests
{
[Fact]
public void GenerateSimpleBinding()
{
var source = """
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 = (Binding)steps["BindingsWithDiagnostics"][0].Outputs[0].Value;

var sourceCodeLocation = new SourceCodeLocation("", 2, 7);

var expectedBinding = new Binding(
1,
sourceCodeLocation,
new TypeName("string", false, false),
new TypeName("int", false, false),
[
new PathPart("s", false),
new PathPart("Length", false),
],
true
);
Assert.Equivalent(expectedBinding, actualBinding);
}

[Fact]
public void GenerateBindingWithLongerPath()
{
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 = (Binding)steps["BindingsWithDiagnostics"][0].Outputs[0].Value;

var sourceCodeLocation = new SourceCodeLocation("", 3, 7);

var expectedBinding = new Binding(
2,
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
);
Assert.Equivalent(expectedBinding, actualBinding);
}

}
@@ -1,5 +1,4 @@
using System.Reflection;
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand All @@ -11,16 +10,17 @@ internal static class SourceGenHelpers
internal static GeneratorDriverRunResult Run(string source)
{
var inputCompilation = CreateCompilation(source);
var driver = CSharpGeneratorDriver.Create(new BindingSourceGenerator());
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),
],
new CSharpCompilationOptions(OutputKind.ConsoleApplication));
}

0 comments on commit 2286ee4

Please sign in to comment.