mirror of
https://github.com/zoriya/EntityFrameworkCore.Projectables.git
synced 2026-08-15 18:44:57 +00:00
Rewrote ExpressionSyntaxRewriter to handle @this rewrites more reliably
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Operations;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -96,88 +97,79 @@ namespace EntityFrameworkCore.Projectables.Generator
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SyntaxNode? VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
|
public override SyntaxNode? VisitThisExpression(ThisExpressionSyntax node)
|
||||||
{
|
{
|
||||||
var symbolInfo = _semanticModel.GetSymbolInfo(node);
|
// Swap out the use of this to @this
|
||||||
|
return SyntaxFactory.IdentifierName("@this");
|
||||||
|
}
|
||||||
|
|
||||||
if (symbolInfo.Symbol is not null)
|
public override SyntaxNode? VisitBaseExpression(BaseExpressionSyntax node)
|
||||||
{
|
{
|
||||||
var scopedNode = node.ChildNodes().FirstOrDefault();
|
// Swap out the use of this to @this
|
||||||
if (scopedNode is ThisExpressionSyntax)
|
return SyntaxFactory.IdentifierName("@this");
|
||||||
{
|
|
||||||
var nextNode = node.ChildNodes().Skip(1).FirstOrDefault() as SimpleNameSyntax;
|
|
||||||
|
|
||||||
if (nextNode is not null)
|
|
||||||
{
|
|
||||||
return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
|
|
||||||
SyntaxFactory.IdentifierName("@this"),
|
|
||||||
nextNode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.VisitMemberAccessExpression(node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
|
public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
|
||||||
{
|
{
|
||||||
var symbol = _semanticModel.GetSymbolInfo(node).Symbol;
|
var symbol = _semanticModel.GetSymbolInfo(node).Symbol;
|
||||||
|
|
||||||
if (symbol is not null)
|
if (symbol is not null)
|
||||||
{
|
{
|
||||||
if (symbol is IMethodSymbol methodSymbol && methodSymbol.IsExtensionMethod)
|
var operation = node switch {
|
||||||
|
{ Parent: { } parent } when parent.IsKind(SyntaxKind.InvocationExpression) => _semanticModel.GetOperation(node.Parent),
|
||||||
|
_ => _semanticModel.GetOperation(node!)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (operation is IMemberReferenceOperation memberReferenceOperation)
|
||||||
{
|
{
|
||||||
// Ignore extension methods
|
var memberAccessCanBeQualified = node switch {
|
||||||
|
{ Parent: { Parent: { } parent } } when parent.IsKind(SyntaxKind.ObjectInitializerExpression) => false,
|
||||||
|
_ => true
|
||||||
|
};
|
||||||
|
|
||||||
|
if (memberAccessCanBeQualified)
|
||||||
|
{
|
||||||
|
// if this operation is targeting an instance member on our targetType implicitly
|
||||||
|
if (memberReferenceOperation.Instance is { IsImplicit: true } && SymbolEqualityComparer.Default.Equals(memberReferenceOperation.Instance.Type, _targetTypeSymbol))
|
||||||
|
{
|
||||||
|
return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
|
||||||
|
SyntaxFactory.IdentifierName("@this"),
|
||||||
|
node.WithoutLeadingTrivia()
|
||||||
|
).WithLeadingTrivia(node.GetLeadingTrivia());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this operation is targeting a static member on our targetType implicitly
|
||||||
|
if (memberReferenceOperation.Instance is null && SymbolEqualityComparer.Default.Equals(memberReferenceOperation.Member.ContainingType, _targetTypeSymbol))
|
||||||
|
{
|
||||||
|
return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
|
||||||
|
SyntaxFactory.ParseTypeName(_targetTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||||
|
node.WithoutLeadingTrivia()
|
||||||
|
).WithLeadingTrivia(node.GetLeadingTrivia());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (symbol.Kind is SymbolKind.Property or SymbolKind.Method or SymbolKind.Field)
|
else if (operation is IInvocationOperation invocationOperation)
|
||||||
{
|
{
|
||||||
// We may need to rewrite this expression such that it refers to our @this argument
|
// if this operation is targeting an instance method on our targetType implicitly
|
||||||
bool rewrite = true;
|
if (invocationOperation.Instance is { IsImplicit: true } && SymbolEqualityComparer.Default.Equals(invocationOperation.Instance.Type, _targetTypeSymbol))
|
||||||
|
|
||||||
if (node.Parent is MemberAccessExpressionSyntax parentMemberAccessNode)
|
|
||||||
{
|
{
|
||||||
var targetSymbolInfo = _semanticModel.GetSymbolInfo(parentMemberAccessNode.Expression);
|
|
||||||
if (targetSymbolInfo.Symbol is null)
|
|
||||||
{
|
|
||||||
rewrite = false;
|
|
||||||
}
|
|
||||||
else if (targetSymbolInfo.Symbol is { Kind: SymbolKind.Parameter or SymbolKind.NamedType })
|
|
||||||
{
|
|
||||||
rewrite = false;
|
|
||||||
}
|
|
||||||
else if (targetSymbolInfo.Symbol?.ContainingType is not null)
|
|
||||||
{
|
|
||||||
if (!_compilation.HasImplicitConversion(targetSymbolInfo.Symbol.ContainingType, _targetTypeSymbol) ||
|
|
||||||
!SymbolEqualityComparer.Default.Equals(symbol.ContainingType, _targetTypeSymbol))
|
|
||||||
{
|
|
||||||
rewrite = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (node.Parent.IsKind(SyntaxKind.SimpleAssignmentExpression))
|
|
||||||
{
|
|
||||||
rewrite = false;
|
|
||||||
}
|
|
||||||
else if (node.Parent.IsKind(SyntaxKind.InvocationExpression))
|
|
||||||
{
|
|
||||||
rewrite = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rewrite)
|
|
||||||
{
|
|
||||||
var expressionSyntax = symbol.IsStatic
|
|
||||||
? SyntaxFactory.ParseTypeName(_targetTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
|
|
||||||
: SyntaxFactory.IdentifierName("@this");
|
|
||||||
|
|
||||||
return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
|
return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
|
||||||
expressionSyntax,
|
SyntaxFactory.IdentifierName("@this"),
|
||||||
node
|
node.WithoutLeadingTrivia()
|
||||||
);
|
).WithLeadingTrivia(node.GetLeadingTrivia());
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this operation is targeting a static method on our targetType implicitly
|
||||||
|
if (invocationOperation.Instance is null && SymbolEqualityComparer.Default.Equals(invocationOperation.TargetMethod.ContainingType, _targetTypeSymbol))
|
||||||
|
{
|
||||||
|
return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
|
||||||
|
SyntaxFactory.ParseTypeName(_targetTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
|
||||||
|
node.WithoutLeadingTrivia()
|
||||||
|
).WithLeadingTrivia(node.GetLeadingTrivia());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (symbol.Kind is SymbolKind.NamedType && node.Parent?.Kind() is not SyntaxKind.QualifiedName)
|
|
||||||
|
// if this node refers to a named type which is not yet fully qualified, we want to fully qualify it
|
||||||
|
if (symbol.Kind is SymbolKind.NamedType && node.Parent?.Kind() is not SyntaxKind.QualifiedName)
|
||||||
{
|
{
|
||||||
var typeInfo = _semanticModel.GetTypeInfo(node);
|
var typeInfo = _semanticModel.GetTypeInfo(node);
|
||||||
|
|
||||||
@@ -185,11 +177,11 @@ namespace EntityFrameworkCore.Projectables.Generator
|
|||||||
{
|
{
|
||||||
return SyntaxFactory.ParseTypeName(
|
return SyntaxFactory.ParseTypeName(
|
||||||
typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
||||||
);
|
).WithLeadingTrivia(node.GetLeadingTrivia());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.VisitIdentifierName(node);
|
return base.VisitIdentifierName(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using Projectables.Repro;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projectables.Generated
|
||||||
|
#nullable disable
|
||||||
|
{
|
||||||
|
public static class Projectables_Repro_Derived_Bar
|
||||||
|
{
|
||||||
|
public static System.Linq.Expressions.Expression<System.Func<global::Projectables.Repro.Derived, string>> Expression()
|
||||||
|
{
|
||||||
|
return (global::Projectables.Repro.Derived @this) =>
|
||||||
|
@this.Foo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using Projectables.Repro;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projectables.Generated
|
||||||
|
#nullable disable
|
||||||
|
{
|
||||||
|
public static class Projectables_Repro_Derived_Bar
|
||||||
|
{
|
||||||
|
public static System.Linq.Expressions.Expression<System.Func<global::Projectables.Repro.Derived, string>> Expression()
|
||||||
|
{
|
||||||
|
return (global::Projectables.Repro.Derived @this) =>
|
||||||
|
@this.Foo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using Projectables.Repro;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projectables.Generated
|
||||||
|
#nullable disable
|
||||||
|
{
|
||||||
|
public static class Projectables_Repro_Derived_Bar
|
||||||
|
{
|
||||||
|
public static System.Linq.Expressions.Expression<System.Func<global::Projectables.Repro.Derived, string>> Expression()
|
||||||
|
{
|
||||||
|
return (global::Projectables.Repro.Derived @this) =>
|
||||||
|
@this.Foo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using Projectables.Repro;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projectables.Generated
|
||||||
|
#nullable disable
|
||||||
|
{
|
||||||
|
public static class Projectables_Repro_Derived_Bar
|
||||||
|
{
|
||||||
|
public static System.Linq.Expressions.Expression<System.Func<global::Projectables.Repro.Derived, string>> Expression()
|
||||||
|
{
|
||||||
|
return (global::Projectables.Repro.Derived @this) =>
|
||||||
|
@this.Foo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Projectables.Repro;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projectables.Generated
|
||||||
|
#nullable disable
|
||||||
|
{
|
||||||
|
public static class Projectables_Repro_SomeEntity_FooOrBar
|
||||||
|
{
|
||||||
|
public static System.Linq.Expressions.Expression<System.Func<global::Projectables.Repro.SomeEntity, string>> Expression()
|
||||||
|
{
|
||||||
|
return (global::Projectables.Repro.SomeEntity @this) =>
|
||||||
|
@this.Foo != null ? @this.Foo : @this.Bar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Projectables.Repro;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projectables.Generated
|
||||||
|
#nullable disable
|
||||||
|
{
|
||||||
|
public static class Projectables_Repro_SomeEntity_RootChildren
|
||||||
|
{
|
||||||
|
public static System.Linq.Expressions.Expression<System.Func<global::Projectables.Repro.SomeEntity, ICollection<global::Projectables.Repro.SomeEntity>>> Expression()
|
||||||
|
{
|
||||||
|
return (global::Projectables.Repro.SomeEntity @this) =>
|
||||||
|
@this.Parent != null ? @this.Parent.RootChildren : @this.Children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Projectables.Repro;
|
||||||
|
|
||||||
|
namespace EntityFrameworkCore.Projectables.Generated
|
||||||
|
#nullable disable
|
||||||
|
{
|
||||||
|
public static class Projectables_Repro_SomeEntity_ParentOrThis
|
||||||
|
{
|
||||||
|
public static System.Linq.Expressions.Expression<System.Func<global::Projectables.Repro.SomeEntity, global::Projectables.Repro.SomeEntity>> Expression()
|
||||||
|
{
|
||||||
|
return (global::Projectables.Repro.SomeEntity @this) =>
|
||||||
|
@this. Parent != null ? @this.Parent : this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+174
@@ -1227,6 +1227,180 @@ public static class SomeExtensions
|
|||||||
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task NavigationProperties()
|
||||||
|
{
|
||||||
|
var compilation = CreateCompilation(@"
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Projectables.Repro;
|
||||||
|
|
||||||
|
public class SomeEntity
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public SomeEntity Parent { get; set; }
|
||||||
|
|
||||||
|
public ICollection<SomeEntity> Children { get; set; }
|
||||||
|
|
||||||
|
[Projectable]
|
||||||
|
public ICollection<SomeEntity> RootChildren =>
|
||||||
|
Parent != null ? Parent.RootChildren : Children;
|
||||||
|
}
|
||||||
|
");
|
||||||
|
|
||||||
|
var result = RunGenerator(compilation);
|
||||||
|
|
||||||
|
Assert.Empty(result.Diagnostics);
|
||||||
|
Assert.Single(result.GeneratedTrees);
|
||||||
|
|
||||||
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task FooOrBar()
|
||||||
|
{
|
||||||
|
var compilation = CreateCompilation(@"
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Projectables.Repro;
|
||||||
|
|
||||||
|
public class SomeEntity
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Foo { get; set; }
|
||||||
|
|
||||||
|
public string Bar { get; set; }
|
||||||
|
|
||||||
|
[Projectable]
|
||||||
|
public string FooOrBar =>
|
||||||
|
Foo != null ? Foo : Bar;
|
||||||
|
}
|
||||||
|
");
|
||||||
|
|
||||||
|
var result = RunGenerator(compilation);
|
||||||
|
|
||||||
|
Assert.Empty(result.Diagnostics);
|
||||||
|
Assert.Single(result.GeneratedTrees);
|
||||||
|
|
||||||
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task BaseMemberExplicitReference()
|
||||||
|
{
|
||||||
|
var compilation = CreateCompilation(@"
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
|
||||||
|
namespace Projectables.Repro;
|
||||||
|
|
||||||
|
class Base
|
||||||
|
{
|
||||||
|
public string Foo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived : Base
|
||||||
|
{
|
||||||
|
[Projectable]
|
||||||
|
public string Bar => base.Foo;
|
||||||
|
}
|
||||||
|
");
|
||||||
|
|
||||||
|
var result = RunGenerator(compilation);
|
||||||
|
|
||||||
|
Assert.Empty(result.Diagnostics);
|
||||||
|
Assert.Single(result.GeneratedTrees);
|
||||||
|
|
||||||
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task BaseMemberImplicitReference()
|
||||||
|
{
|
||||||
|
var compilation = CreateCompilation(@"
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
|
||||||
|
namespace Projectables.Repro;
|
||||||
|
|
||||||
|
class Base
|
||||||
|
{
|
||||||
|
public string Foo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived : Base
|
||||||
|
{
|
||||||
|
[Projectable]
|
||||||
|
public string Bar => Foo;
|
||||||
|
}
|
||||||
|
");
|
||||||
|
|
||||||
|
var result = RunGenerator(compilation);
|
||||||
|
|
||||||
|
Assert.Empty(result.Diagnostics);
|
||||||
|
Assert.Single(result.GeneratedTrees);
|
||||||
|
|
||||||
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task BaseMethodExplicitReference()
|
||||||
|
{
|
||||||
|
var compilation = CreateCompilation(@"
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
|
||||||
|
namespace Projectables.Repro;
|
||||||
|
|
||||||
|
class Base
|
||||||
|
{
|
||||||
|
public string Foo() => """";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived : Base
|
||||||
|
{
|
||||||
|
[Projectable]
|
||||||
|
public string Bar => base.Foo();
|
||||||
|
}
|
||||||
|
");
|
||||||
|
|
||||||
|
var result = RunGenerator(compilation);
|
||||||
|
|
||||||
|
Assert.Empty(result.Diagnostics);
|
||||||
|
Assert.Single(result.GeneratedTrees);
|
||||||
|
|
||||||
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task BaseMethorImplicitReference()
|
||||||
|
{
|
||||||
|
var compilation = CreateCompilation(@"
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
|
||||||
|
namespace Projectables.Repro;
|
||||||
|
|
||||||
|
class Base
|
||||||
|
{
|
||||||
|
public string Foo() => """";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Derived : Base
|
||||||
|
{
|
||||||
|
[Projectable]
|
||||||
|
public string Bar => Foo();
|
||||||
|
}
|
||||||
|
");
|
||||||
|
|
||||||
|
var result = RunGenerator(compilation);
|
||||||
|
|
||||||
|
Assert.Empty(result.Diagnostics);
|
||||||
|
Assert.Single(result.GeneratedTrees);
|
||||||
|
|
||||||
|
return Verifier.Verify(result.GeneratedTrees[0].ToString());
|
||||||
|
}
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
Compilation CreateCompilation(string source, bool expectedToCompile = true)
|
Compilation CreateCompilation(string source, bool expectedToCompile = true)
|
||||||
|
|||||||
Reference in New Issue
Block a user