Skip to content

Commit

Permalink
Add C-Style casts support (#26)
Browse files Browse the repository at this point in the history
* initialized cstyle casts

* added integration and accessExpressionBuilder tests

* added explicit cast to as cast adapter

* Simplify inserting conditional access

* Add integration tests

---------

Co-authored-by: Simon Rozsival <[email protected]>
  • Loading branch information
jkurdek and simonrozsival authored Apr 30, 2024
1 parent d411076 commit a573668
Show file tree
Hide file tree
Showing 8 changed files with 551 additions and 123 deletions.
30 changes: 21 additions & 9 deletions src/Controls/src/BindingSourceGen/BindingCodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,25 +287,37 @@ private void AppendHandlersArray(TypeDescription sourceType, EquatableArray<IPat
Indent();

string nextExpression = "source";
bool forceConditonalAccessToNextPart = false;
foreach (var part in path)
{
var expression = nextExpression;
nextExpression = AccessExpressionBuilder.Build(nextExpression, part);
var previousExpression = nextExpression;
nextExpression = AccessExpressionBuilder.Build(previousExpression, MaybeWrapInConditionalAccess(part, forceConditonalAccessToNextPart));
forceConditonalAccessToNextPart = part is Cast;

// Some parts don't have a property name, so we can't generate a handler for them (for example casts)
var propertyName = part.PropertyName;
if (propertyName is null)
if (part.PropertyName is string propertyName)
{
continue;
AppendLine($"new(static source => {previousExpression}, \"{propertyName}\"),");
}

Append("new(static source => ");
Append(expression);
AppendLine($", \"{part.PropertyName}\"),");
}
Unindent();

Append('}');

static IPathPart MaybeWrapInConditionalAccess(IPathPart part, bool forceConditonalAccess)
{
if (!forceConditonalAccess)
{
return part;
}

return part switch
{
MemberAccess memberAccess => new ConditionalAccess(memberAccess),
IndexAccess indexAccess => new ConditionalAccess(indexAccess),
_ => part,
};
}
}

public void Dispose()
Expand Down
100 changes: 2 additions & 98 deletions src/Controls/src/BindingSourceGen/BindingSourceGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.Maui.Controls.BindingSourceGen;

Expand Down Expand Up @@ -90,7 +89,7 @@ static BindingDiagnosticsWrapper GetBindingForGeneration(GeneratorSyntaxContext
return ReportDiagnostic(DiagnosticsFactory.UnableToResolvePath(lambdaBody.GetLocation()));
}

var pathParser = new PathParser(context);
var pathParser = new PathParser(context, enabledNullable);
var (pathDiagnostics, parts) = pathParser.ParsePath(lambdaBody);
if (pathDiagnostics.Length > 0)
{
Expand Down Expand Up @@ -179,6 +178,7 @@ private static SetterOptions DeriveSetterOptions(ExpressionSyntax? lambdaBodyExp
ConditionalAccessExpressionSyntax conditionalAccess => conditionalAccess.WhenNotNull,
MemberBindingExpressionSyntax memberBinding => memberBinding.Name,
BinaryExpressionSyntax binary when binary.Kind() == SyntaxKind.AsExpression => binary.Left,
CastExpressionSyntax cast => cast.Expression,
ParenthesizedExpressionSyntax parenthesized => parenthesized.Expression,
_ => null,
};
Expand All @@ -205,99 +205,3 @@ static bool AcceptsNullValue(ISymbol? symbol, bool enabledNullable)
private static BindingDiagnosticsWrapper ReportDiagnostics(EquatableArray<DiagnosticInfo> diagnostics) => new(null, diagnostics);
private static BindingDiagnosticsWrapper ReportDiagnostic(DiagnosticInfo diagnostic) => new(null, new EquatableArray<DiagnosticInfo>([diagnostic]));
}

public class TrackingNames
{
public const string BindingsWithDiagnostics = nameof(BindingsWithDiagnostics);
public const string Bindings = nameof(Bindings);
}

public sealed record BindingDiagnosticsWrapper(
CodeWriterBinding? Binding,
EquatableArray<DiagnosticInfo> Diagnostics);

public sealed record CodeWriterBinding(
InterceptorLocation Location,
TypeDescription SourceType,
TypeDescription PropertyType,
EquatableArray<IPathPart> Path,
SetterOptions SetterOptions);

public sealed record SourceCodeLocation(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan)
{
public static SourceCodeLocation? CreateFrom(Location location)
=> location.SourceTree is null
? null
: new SourceCodeLocation(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span);

public Location ToLocation()
{
return Location.Create(FilePath, TextSpan, LineSpan);
}

public InterceptorLocation ToInterceptorLocation()
{
return new InterceptorLocation(FilePath, LineSpan.Start.Line + 1, LineSpan.Start.Character + 1);
}
}

public sealed record InterceptorLocation(string FilePath, int Line, int Column);

public sealed record TypeDescription(
string GlobalName,
bool IsValueType = false,
bool IsNullable = false,
bool IsGenericParameter = false)
{
public override string ToString()
=> IsNullable
? $"{GlobalName}?"
: GlobalName;
}

public sealed record SetterOptions(bool IsWritable, bool AcceptsNullValue = false);

public sealed record MemberAccess(string MemberName) : IPathPart
{
public string? PropertyName => MemberName;

public bool Equals(IPathPart other)
{
return other is MemberAccess memberAccess && MemberName == memberAccess.MemberName;
}
}

public sealed record IndexAccess(string DefaultMemberName, object Index) : IPathPart
{
public string? PropertyName => $"{DefaultMemberName}[{Index}]";

public bool Equals(IPathPart other)
{
return other is IndexAccess indexAccess && DefaultMemberName == indexAccess.DefaultMemberName && Index.Equals(indexAccess.Index);
}
}

public sealed record ConditionalAccess(IPathPart Part) : IPathPart
{
public string? PropertyName => Part.PropertyName;

public bool Equals(IPathPart other)
{
return other is ConditionalAccess conditionalAccess && Part.Equals(conditionalAccess.Part);
}
}

public sealed record Cast(TypeDescription TargetType) : IPathPart
{
public string? PropertyName => null;

public bool Equals(IPathPart other)
{
return other is Cast cast && TargetType.Equals(cast.TargetType);
}
}

public interface IPathPart : IEquatable<IPathPart>
{
public string? PropertyName { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,19 @@ internal static TypeDescription CreateTypeNameFromITypeSymbol(ITypeSymbol typeSy
IsValueType: typeSymbol.IsValueType);
}

internal static TypeDescription CreateTypeDescriptionForCast(ITypeSymbol typeSymbol)
internal static TypeDescription CreateTypeDescriptionForExplicitCast(ITypeSymbol typeSymbol, bool enabledNullable)
{
var isNullable = IsTypeNullable(typeSymbol, enabledNullable);
var name = GetGlobalName(typeSymbol, isNullable, typeSymbol.IsValueType);

return new TypeDescription(
GlobalName: name,
IsNullable: isNullable,
IsGenericParameter: typeSymbol.Kind == SymbolKind.TypeParameter,
IsValueType: typeSymbol.IsValueType);
}

internal static TypeDescription CreateTypeDescriptionForAsCast(ITypeSymbol typeSymbol)
{
// We can cast to nullable value type or non-nullable reference type
var name = typeSymbol.IsValueType ?
Expand Down
101 changes: 101 additions & 0 deletions src/Controls/src/BindingSourceGen/GeneratorDataModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.Maui.Controls.BindingSourceGen;

public class TrackingNames
{
public const string BindingsWithDiagnostics = nameof(BindingsWithDiagnostics);
public const string Bindings = nameof(Bindings);
}

public sealed record BindingDiagnosticsWrapper(
CodeWriterBinding? Binding,
EquatableArray<DiagnosticInfo> Diagnostics);

public sealed record CodeWriterBinding(
InterceptorLocation Location,
TypeDescription SourceType,
TypeDescription PropertyType,
EquatableArray<IPathPart> Path,
SetterOptions SetterOptions);

public sealed record SourceCodeLocation(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan)
{
public static SourceCodeLocation? CreateFrom(Location location)
=> location.SourceTree is null
? null
: new SourceCodeLocation(location.SourceTree.FilePath, location.SourceSpan, location.GetLineSpan().Span);

public Location ToLocation()
{
return Location.Create(FilePath, TextSpan, LineSpan);
}

public InterceptorLocation ToInterceptorLocation()
{
return new InterceptorLocation(FilePath, LineSpan.Start.Line + 1, LineSpan.Start.Character + 1);
}
}

public sealed record InterceptorLocation(string FilePath, int Line, int Column);

public sealed record TypeDescription(
string GlobalName,
bool IsValueType = false,
bool IsNullable = false,
bool IsGenericParameter = false)
{
public override string ToString()
=> IsNullable
? $"{GlobalName}?"
: GlobalName;
}

public sealed record SetterOptions(bool IsWritable, bool AcceptsNullValue = false);

public sealed record MemberAccess(string MemberName) : IPathPart
{
public string? PropertyName => MemberName;

public bool Equals(IPathPart other)
{
return other is MemberAccess memberAccess && MemberName == memberAccess.MemberName;
}
}

public sealed record IndexAccess(string DefaultMemberName, object Index) : IPathPart
{
public string? PropertyName => $"{DefaultMemberName}[{Index}]";

public bool Equals(IPathPart other)
{
return other is IndexAccess indexAccess && DefaultMemberName == indexAccess.DefaultMemberName && Index.Equals(indexAccess.Index);
}
}

public sealed record ConditionalAccess(IPathPart Part) : IPathPart
{
public string? PropertyName => Part.PropertyName;

public bool Equals(IPathPart other)
{
return other is ConditionalAccess conditionalAccess && Part.Equals(conditionalAccess.Part);
}
}

public sealed record Cast(TypeDescription TargetType) : IPathPart
{
public string? PropertyName => null;

public bool Equals(IPathPart other)
{
return other is Cast cast && TargetType.Equals(cast.TargetType);
}
}

public interface IPathPart : IEquatable<IPathPart>
{
public string? PropertyName { get; }
}
25 changes: 23 additions & 2 deletions src/Controls/src/BindingSourceGen/PathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ namespace Microsoft.Maui.Controls.BindingSourceGen;

internal class PathParser
{
internal PathParser(GeneratorSyntaxContext context)
internal PathParser(GeneratorSyntaxContext context, bool enabledNullable)
{
Context = context;
EnabledNullable = enabledNullable;
}

private GeneratorSyntaxContext Context { get; }
private bool EnabledNullable { get; }

internal (EquatableArray<DiagnosticInfo> diagnostics, List<IPathPart> parts) ParsePath(CSharpSyntaxNode? expressionSyntax)
{
Expand All @@ -26,6 +28,7 @@ internal PathParser(GeneratorSyntaxContext context)
MemberBindingExpressionSyntax memberBinding => HandleMemberBindingExpression(memberBinding),
ParenthesizedExpressionSyntax parenthesized => ParsePath(parenthesized.Expression),
BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExpression => HandleBinaryExpression(asExpression),
CastExpressionSyntax castExpression => HandleCastExpression(castExpression),
_ => HandleDefaultCase(),
};
}
Expand Down Expand Up @@ -118,7 +121,25 @@ BinaryExpressionSyntax asExpression when asExpression.Kind() == SyntaxKind.AsExp
return (new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List<IPathPart>());
};

parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForCast(typeInfo)));
parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForAsCast(typeInfo)));
return (diagnostics, parts);
}

private (EquatableArray<DiagnosticInfo> diagnostics, List<IPathPart> parts) HandleCastExpression(CastExpressionSyntax castExpression)
{
var (diagnostics, parts) = ParsePath(castExpression.Expression);
if (diagnostics.Length > 0)
{
return (diagnostics, parts);
}

var typeInfo = Context.SemanticModel.GetTypeInfo(castExpression.Type).Type;
if (typeInfo == null)
{
return (new EquatableArray<DiagnosticInfo>([DiagnosticsFactory.UnableToResolvePath(Context.Node.GetLocation())]), new List<IPathPart>());
};

parts.Add(new Cast(BindingGenerationUtilities.CreateTypeDescriptionForExplicitCast(typeInfo, EnabledNullable)));
return (diagnostics, parts);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Linq;
using Microsoft.Maui.Controls.BindingSourceGen;
using Xunit;

Expand Down
Loading

0 comments on commit a573668

Please sign in to comment.