From a3ee32193681a34975b618a5482ee9f7f0ed5b7d Mon Sep 17 00:00:00 2001 From: Koen Bekkenutte <2912652+kbekkenutte@users.noreply.github.com> Date: Wed, 20 Apr 2022 23:40:20 +0800 Subject: [PATCH] Support for const and static in projectables --- .../Diagnostics.cs | 4 +- .../ExpressionSyntaxRewriter.cs | 21 ++- ...s.FilterOnProjectableProperty.verified.txt | 3 + .../StaticMembersTests.cs | 45 ++++++ ...ionGeneratorTests.ConstMember.verified.txt | 18 +++ ...onGeneratorTests.ConstMember2.verified.txt | 18 +++ ...onGeneratorTests.ConstMember3.verified.txt | 18 +++ ...ratorTests.ConstantExpression.received.txt | 18 +++ ...ratorTests.ConstantExpression.verified.txt | 0 ...atorTests.ConstantExpression2.received.txt | 18 +++ ...atorTests.ConstantExpression2.verified.txt | 18 +++ ...atorTests.ConstantExpressions.received.txt | 18 +++ ...atorTests.ConstantExpressions.verified.txt | 0 ...nGeneratorTests.StaticMembers.verified.txt | 18 +++ ...GeneratorTests.StaticMembers2.verified.txt | 18 +++ .../ProjectionExpressionGeneratorTests.cs | 149 ++++++++++++++++++ 16 files changed, 375 insertions(+), 9 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.FilterOnProjectableProperty.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember2.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember3.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression.received.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression2.received.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression2.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpressions.received.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpressions.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.StaticMembers.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.StaticMembers2.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs index b1ef422..18f87c1 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs @@ -9,7 +9,7 @@ namespace EntityFrameworkCore.Projectables.Generator { public static class Diagnostics { - public static readonly DiagnosticDescriptor RequiresExpressionBodyDefinition = new( + public static readonly DiagnosticDescriptor RequiresExpressionBodyDefinition = new DiagnosticDescriptor( id: "EFP0001", title: "Method or property should expose an expression body definition", messageFormat: "Method or property '{0}' should expose an expression body definition", @@ -17,7 +17,7 @@ namespace EntityFrameworkCore.Projectables.Generator DiagnosticSeverity.Error, isEnabledByDefault: true); - public static readonly DiagnosticDescriptor NullConditionalRewriteUnsupported = new( + public static readonly DiagnosticDescriptor NullConditionalRewriteUnsupported = new DiagnosticDescriptor( id: "EFP0002", title: "Method or property is not configured to support null-conditional expressions", messageFormat: "'{0}' has a null-conditional expression exposed but is not configured to rewrite this (Consider configuring a strategy using the NullConditionalRewriteSupport property on the Projectable attribute)", diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index a04c566..35c6931 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -122,15 +122,17 @@ namespace EntityFrameworkCore.Projectables.Generator public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) { - var symbolInfo = _semanticModel.GetSymbolInfo(node); + var symbol = _semanticModel.GetSymbolInfo(node).Symbol; - if (symbolInfo.Symbol is not null) + if (symbol is not null) { - if (symbolInfo.Symbol is IMethodSymbol methodSymbol && methodSymbol.IsExtensionMethod) + if (symbol is IMethodSymbol methodSymbol && methodSymbol.IsExtensionMethod) { + // Ignore extension methods } - else if (symbolInfo.Symbol.Kind is SymbolKind.Property or SymbolKind.Method or SymbolKind.Field) + else if (symbol.Kind is SymbolKind.Property or SymbolKind.Method or SymbolKind.Field) { + // We may need to rewrite this expression such that it refers to our @this argument bool rewrite = true; if (node.Parent is MemberAccessExpressionSyntax parentMemberAccessNode) @@ -147,7 +149,7 @@ namespace EntityFrameworkCore.Projectables.Generator else if (targetSymbolInfo.Symbol?.ContainingType is not null) { if (!_compilation.HasImplicitConversion(targetSymbolInfo.Symbol.ContainingType, _targetTypeSymbol) || - !SymbolEqualityComparer.Default.Equals(symbolInfo.Symbol.ContainingType, _targetTypeSymbol)) + !SymbolEqualityComparer.Default.Equals(symbol.ContainingType, _targetTypeSymbol)) { rewrite = false; } @@ -164,13 +166,18 @@ namespace EntityFrameworkCore.Projectables.Generator if (rewrite) { + var expressionSyntax = symbol.IsStatic + ? SyntaxFactory.ParseTypeName(_targetTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) + : SyntaxFactory.IdentifierName("@this"); + return SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("@this"), + expressionSyntax, node ); } + } - else if (symbolInfo.Symbol.Kind is SymbolKind.NamedType && node.Parent?.Kind() is not SyntaxKind.QualifiedName) + else if (symbol.Kind is SymbolKind.NamedType && node.Parent?.Kind() is not SyntaxKind.QualifiedName) { var typeInfo = _semanticModel.GetTypeInfo(node); diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.FilterOnProjectableProperty.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.FilterOnProjectableProperty.verified.txt new file mode 100644 index 0000000..18a63b4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.FilterOnProjectableProperty.verified.txt @@ -0,0 +1,3 @@ +SELECT [e].[Id], [e].[Price] +FROM [Entity] AS [e] +WHERE [e].[Price] > 1.0E0 \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs new file mode 100644 index 0000000..74e10b4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/StaticMembersTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlTypes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projectables.Extensions; +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using ScenarioTests; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.FunctionalTests +{ + [UsesVerify] + public class StaticMembersTests + { + public static class Constants + { + public static readonly double ExpensiveThreshold = 1; + } + + public record Entity + { + public int Id { get; set; } + + public double Price { get; set; } + + [Projectable] + public bool IsExpensive => Price > Constants.ExpensiveThreshold; + } + + [Fact] + public Task FilterOnProjectableProperty() + { + using var dbContext = new SampleDbContext(Infrastructure.CompatibilityMode.Full); + + var query = dbContext.Set() + .Where(x => x.IsExpensive); + + return Verifier.Verify(query.ToQueryString()); + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember.verified.txt new file mode 100644 index 0000000..985c66c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember.verified.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id + global::Foo.Foo.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember2.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember2.verified.txt new file mode 100644 index 0000000..a0ad5af --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember2.verified.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id + global::Foo.Constants.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember3.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember3.verified.txt new file mode 100644 index 0000000..985c66c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstMember3.verified.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id + global::Foo.Foo.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression.received.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression.received.txt new file mode 100644 index 0000000..dbcb126 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression.received.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id + @this.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression.verified.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression2.received.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression2.received.txt new file mode 100644 index 0000000..8605e73 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression2.received.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id > global::Foo.Constants.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression2.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression2.verified.txt new file mode 100644 index 0000000..a0ad5af --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpression2.verified.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id + global::Foo.Constants.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpressions.received.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpressions.received.txt new file mode 100644 index 0000000..dbcb126 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpressions.received.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id + @this.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpressions.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ConstantExpressions.verified.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.StaticMembers.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.StaticMembers.verified.txt new file mode 100644 index 0000000..985c66c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.StaticMembers.verified.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id + global::Foo.Foo.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.StaticMembers2.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.StaticMembers2.verified.txt new file mode 100644 index 0000000..a0ad5af --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.StaticMembers2.verified.txt @@ -0,0 +1,18 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +#nullable disable +{ + public static class Foo_Foo_IdWithBar + { + public static System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Foo @this) => + @this.Id + global::Foo.Constants.Bar; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index b264a71..ec1ba18 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -988,6 +988,155 @@ namespace Foo { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task StaticMembers() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + public static int Bar { get; set; } + + public int Id { get; set; } + + [Projectable] + public int IdWithBar() => Id + Bar; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task StaticMembers2() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public static class Constants { + public static readonly int Bar = 1; + } + + public class Foo { + public int Id { get; set; } + + [Projectable] + public int IdWithBar() => Id + Constants.Bar; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ConstMember() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + public const int Bar = 1; + + public int Id { get; set; } + + [Projectable] + public int IdWithBar() => Id + Bar; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ConstMember2() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public static class Constants { + public const int Bar = 1; + } + + public class Foo { + public int Id { get; set; } + + [Projectable] + public int IdWithBar() => Id + Constants.Bar; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ConstMember3() + { + var compilation = CreateCompilation(@" +using System; +using System.Linq; +using System.Collections.Generic; +using EntityFrameworkCore.Projectables; + +namespace Foo { + public class Foo { + public const int Bar = 1; + + public int Id { get; set; } + + [Projectable] + public int IdWithBar() => Id + Foo.Bar; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + [Fact] public Task RelationalProperty() {