diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md index 5f28270..5f7773d 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md +++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md @@ -1 +1,6 @@ - \ No newline at end of file +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|----------------------------------------------- +EFP0007 | Design | Error | Unsupported pattern in projectable expression + diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs index 70e2964..9b968a3 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs @@ -52,5 +52,13 @@ public static class Diagnostics DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor UnsupportedPatternInExpression = new DiagnosticDescriptor( + id: "EFP0007", + title: "Unsupported pattern in projectable expression", + messageFormat: "The pattern '{0}' cannot be rewritten into an expression tree. Simplify the pattern or restructure the projectable member body.", + category: "Design", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index f953701..ae58790 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -309,130 +309,56 @@ private ExpressionSyntax CreateMethodCallOnEnumValue(IMethodSymbol methodSymbol, public override SyntaxNode? VisitSwitchExpression(SwitchExpressionSyntax node) { - // Reverse arms order to start from the default value var arms = node.Arms.Reverse(); - + var visitedGoverning = (ExpressionSyntax)Visit(node.GoverningExpression); ExpressionSyntax? currentExpression = null; foreach (var arm in arms) { var armExpression = (ExpressionSyntax)Visit(arm.Expression); - - // Handle fallback value + if (currentExpression == null) { - currentExpression = arm.Pattern is DiscardPatternSyntax + currentExpression = arm.Pattern is DiscardPatternSyntax ? armExpression : SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression); - continue; } - - // Handle each arm, only if it's a constant expression - if (arm.Pattern is ConstantPatternSyntax constant) - { - ExpressionSyntax expression = SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression, (ExpressionSyntax)Visit(node.GoverningExpression), constant.Expression); - - // Add the when clause as a AND expression - if (arm.WhenClause != null) - { - expression = SyntaxFactory.BinaryExpression( - SyntaxKind.LogicalAndExpression, - expression, - (ExpressionSyntax)Visit(arm.WhenClause.Condition) - ); - } - - currentExpression = SyntaxFactory.ConditionalExpression( - expression, - armExpression, - currentExpression - ); - continue; - } + ExpressionSyntax? condition; - if (arm.Pattern is DeclarationPatternSyntax declaration) + // DeclarationPattern with a named variable requires replacing the variable with a cast in the arm body + if (arm.Pattern is DeclarationPatternSyntax declaration && declaration.Designation is SingleVariableDesignationSyntax) { - var getTypeExpression = SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - (ExpressionSyntax)Visit(node.GoverningExpression), - SyntaxFactory.IdentifierName("GetType") - ); - - var getTypeCall = SyntaxFactory.InvocationExpression(getTypeExpression); - var typeofExpression = SyntaxFactory.TypeOfExpression(declaration.Type); - var equalsExpression = SyntaxFactory.BinaryExpression( - SyntaxKind.EqualsExpression, - getTypeCall, - typeofExpression - ); - - ExpressionSyntax condition = equalsExpression; - if (arm.WhenClause != null) - { - condition = SyntaxFactory.BinaryExpression( - SyntaxKind.LogicalAndExpression, - equalsExpression, - (ExpressionSyntax)Visit(arm.WhenClause.Condition) - ); - } - - var modifiedArmExpression = ReplaceVariableWithCast(armExpression, declaration, node.GoverningExpression); - currentExpression = SyntaxFactory.ConditionalExpression( - condition, - modifiedArmExpression, - currentExpression - ); - - continue; + condition = SyntaxFactory.BinaryExpression(SyntaxKind.IsExpression, visitedGoverning, declaration.Type); + armExpression = ReplaceVariableWithCast(armExpression, declaration, visitedGoverning); } - - // Handle relational patterns (<=, <, >=, >) - if (arm.Pattern is RelationalPatternSyntax relational) + else { - // Map the pattern operator token to a binary expression kind - var binaryKind = relational.OperatorToken.Kind() switch + condition = ConvertPatternToExpression(arm.Pattern, visitedGoverning); + if (condition is null) { - SyntaxKind.LessThanToken => SyntaxKind.LessThanExpression, - SyntaxKind.LessThanEqualsToken => SyntaxKind.LessThanOrEqualExpression, - SyntaxKind.GreaterThanToken => SyntaxKind.GreaterThanExpression, - SyntaxKind.GreaterThanEqualsToken => SyntaxKind.GreaterThanOrEqualExpression, - _ => throw new InvalidOperationException( - $"Unsupported relational operator in switch expression: {relational.OperatorToken.Kind()}") - }; - - var condition = SyntaxFactory.BinaryExpression( - binaryKind, - (ExpressionSyntax)Visit(node.GoverningExpression), - (ExpressionSyntax)Visit(relational.Expression) - ); - - // Add the when clause as a AND expression - if (arm.WhenClause != null) - { - condition = SyntaxFactory.BinaryExpression( - SyntaxKind.LogicalAndExpression, - condition, - (ExpressionSyntax)Visit(arm.WhenClause.Condition) - ); + // A diagnostic (EFP0007) has already been reported for this arm. + // Skip it instead of falling back to base.VisitSwitchExpression which + // would leave an unsupported switch expression in the generated lambda and + // produce unrelated compiler errors. The best-effort ternary chain built + // so far is still emitted so the output remains valid C#. + continue; } + } - currentExpression = SyntaxFactory.ConditionalExpression( + if (arm.WhenClause != null) + { + condition = SyntaxFactory.BinaryExpression( + SyntaxKind.LogicalAndExpression, condition, - armExpression, - currentExpression + (ExpressionSyntax)Visit(arm.WhenClause.Condition) ); - - continue; } - throw new InvalidOperationException( - $"Switch expressions rewriting supports constant values, relational patterns (<=, <, >=, >), and declaration patterns (Type var). " + - $"Unsupported pattern: {arm.Pattern.GetType().Name}" - ); + currentExpression = SyntaxFactory.ConditionalExpression(condition, armExpression, currentExpression); } - + return currentExpression; } @@ -676,5 +602,281 @@ private ExpressionSyntax ReplaceVariableWithCast(ExpressionSyntax expression, De return expression; } + + public override SyntaxNode? VisitIsPatternExpression(IsPatternExpressionSyntax node) + { + // Pattern matching is not supported in expression trees (CS8122). + // We need to convert patterns into equivalent expressions. + var expression = (ExpressionSyntax)Visit(node.Expression); + + // ConvertPatternToExpression returns null when the pattern cannot be rewritten and has + // already reported a diagnostic (EFP0007). Return a 'false' literal placeholder so + // the generated lambda stays syntactically valid and no additional CS8122 errors are + // triggered by leaving raw pattern-matching syntax inside an expression tree. + return ConvertPatternToExpression(node.Pattern, expression) + ?? SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression); + } + + /// + /// Returns true when can be compared to null. + /// Accepts a pre-resolved symbol so synthesized (unbound) expression nodes can bypass + /// semantic-model lookup, which would return null for synthesized nodes and cause + /// the method to conservatively (and incorrectly) emit a null-check for value-type properties. + /// + private static bool TypeRequiresNullCheck(ITypeSymbol? type) + { + if (type is null) + { + return true; // conservative: unknown type → assume nullable + } + + // Nullable is a value type whose OriginalDefinition is System.Nullable + if (type.IsValueType && + type.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T) + { + return false; // plain struct / record struct — null check would not compile + } + + return true; + } + + /// + /// Attempts to convert into an ordinary expression that is valid + /// inside an expression tree. Returns null and reports a diagnostic when the pattern + /// cannot be rewritten. + /// + /// The pattern syntax to convert. + /// The expression being tested against the pattern. + /// + /// Pre-resolved type of . When the expression is a synthesized + /// node (not present in the original source) Roslyn cannot bind it, so callers that know the + /// type should pass it here to avoid falling back to the conservative "assume nullable" path. + /// + private ExpressionSyntax? ConvertPatternToExpression(PatternSyntax pattern, ExpressionSyntax expression, ITypeSymbol? expressionType = null) + { + switch (pattern) + { + case RecursivePatternSyntax recursivePattern: + return ConvertRecursivePattern(recursivePattern, expression, expressionType); + + case ConstantPatternSyntax constantPattern: + // e is null / e is 5 + return SyntaxFactory.BinaryExpression( + SyntaxKind.EqualsExpression, + expression, + (ExpressionSyntax)Visit(constantPattern.Expression) + ); + + case DeclarationPatternSyntax declarationPattern: + // e is string _ → type-check only (discard is fine) + // e is string s → we cannot safely rewrite because references to 's' in + // the surrounding expression are outside this node's scope. + if (declarationPattern.Designation is SingleVariableDesignationSyntax) + { + _context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UnsupportedPatternInExpression, + pattern.GetLocation(), + pattern.ToString())); + return null; + } + + return SyntaxFactory.BinaryExpression( + SyntaxKind.IsExpression, + expression, + declarationPattern.Type + ); + + case RelationalPatternSyntax relationalPattern: + { + // e is > 100 + SyntaxKind? binaryKind = relationalPattern.OperatorToken.Kind() switch + { + SyntaxKind.LessThanToken => SyntaxKind.LessThanExpression, + SyntaxKind.LessThanEqualsToken => SyntaxKind.LessThanOrEqualExpression, + SyntaxKind.GreaterThanToken => SyntaxKind.GreaterThanExpression, + SyntaxKind.GreaterThanEqualsToken => SyntaxKind.GreaterThanOrEqualExpression, + _ => null + }; + + if (binaryKind is null) + { + _context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UnsupportedPatternInExpression, + pattern.GetLocation(), + pattern.ToString())); + return null; + } + + return SyntaxFactory.BinaryExpression( + binaryKind.Value, + expression, + (ExpressionSyntax)Visit(relationalPattern.Expression) + ); + } + + case BinaryPatternSyntax binaryPattern: + { + // e is > 10 and < 100 + var left = ConvertPatternToExpression(binaryPattern.Left, expression); + var right = ConvertPatternToExpression(binaryPattern.Right, expression); + + // Propagate failures from either side + if (left is null || right is null) + { + return null; + } + + SyntaxKind? logicalKind = binaryPattern.OperatorToken.Kind() switch + { + SyntaxKind.AndKeyword => SyntaxKind.LogicalAndExpression, + SyntaxKind.OrKeyword => SyntaxKind.LogicalOrExpression, + _ => null + }; + + if (logicalKind is null) + { + _context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UnsupportedPatternInExpression, + pattern.GetLocation(), + pattern.ToString())); + return null; + } + + return SyntaxFactory.BinaryExpression(logicalKind.Value, left, right); + } + + case UnaryPatternSyntax unaryPattern when unaryPattern.OperatorToken.IsKind(SyntaxKind.NotKeyword): + { + // e is not null + var inner = ConvertPatternToExpression(unaryPattern.Pattern, expression); + if (inner is null) + { + return null; + } + + return SyntaxFactory.PrefixUnaryExpression( + SyntaxKind.LogicalNotExpression, + SyntaxFactory.ParenthesizedExpression(inner) + ); + } + + default: + _context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UnsupportedPatternInExpression, + pattern.GetLocation(), + pattern.ToString())); + return null; + } + } + + private ExpressionSyntax? ConvertRecursivePattern(RecursivePatternSyntax recursivePattern, ExpressionSyntax expression, ITypeSymbol? expressionType = null) + { + // Positional / deconstruct patterns (e.g. obj is Point(1, 2)) cannot be rewritten + // into a plain expression tree. Report a diagnostic and bail out. + if (recursivePattern.PositionalPatternClause != null) + { + _context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.UnsupportedPatternInExpression, + recursivePattern.GetLocation(), + recursivePattern.ToString())); + return null; + } + + var conditions = new List(); + + // Null check: only legal (and only necessary) for reference types and nullable value types. + // Emitting "x != null" for a plain struct / record struct would not compile. + // Use the pre-resolved expressionType when available so synthesized nodes (which Roslyn + // cannot bind) are handled correctly instead of falling back to the conservative path. + var typeForNullCheck = expressionType ?? _semanticModel.GetTypeInfo(expression).Type; + if (TypeRequiresNullCheck(typeForNullCheck)) + { + conditions.Add(SyntaxFactory.BinaryExpression( + SyntaxKind.NotEqualsExpression, + expression, + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + )); + } + + // Type check: "obj is SomeType { ... }" — add "expression is SomeType" guard. + TypeSyntax? visitedType = null; + if (recursivePattern.Type != null) + { + visitedType = (TypeSyntax)Visit(recursivePattern.Type); + conditions.Add(SyntaxFactory.BinaryExpression( + SyntaxKind.IsExpression, + expression, + visitedType + )); + } + + // When a concrete type is known, member accesses on sub-patterns must go through a + // cast so the generated code compiles correctly (e.g. ((SomeType)expression).Prop). + var memberBase = visitedType != null + ? SyntaxFactory.ParenthesizedExpression( + SyntaxFactory.CastExpression(visitedType, expression)) + : expression; + + // Handle property sub-patterns: { Prop: value, ... } + if (recursivePattern.PropertyPatternClause != null) + { + foreach (var subpattern in recursivePattern.PropertyPatternClause.Subpatterns) + { + ExpressionSyntax propExpression; + ITypeSymbol? propType = null; + + if (subpattern.NameColon != null) + { + // Look up the property/field type from the original source binding so that + // when the recursive ConvertPatternToExpression call checks TypeRequiresNullCheck + // on the synthesized propExpression it receives the real symbol instead of null. + var memberSymbol = _semanticModel.GetSymbolInfo(subpattern.NameColon.Name).Symbol; + propType = memberSymbol switch + { + IPropertySymbol prop => prop.Type, + IFieldSymbol field => field.Type, + _ => null + }; + + propExpression = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + memberBase, + SyntaxFactory.IdentifierName(subpattern.NameColon.Name.Identifier)); + } + else + { + propExpression = memberBase; + } + + // Pass propType so nested recursive patterns don't misidentify value-type + // properties as nullable when Roslyn can't bind the synthesized node. + var condition = ConvertPatternToExpression(subpattern.Pattern, propExpression, propType); + if (condition is null) + { + return null; // diagnostic already emitted + } + + conditions.Add(condition); + } + } + + if (conditions.Count == 0) + { + return SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression); + } + + // Combine all conditions with && + var result = conditions[0]; + for (var i = 1; i < conditions.Count; i++) + { + result = SyntaxFactory.BinaryExpression( + SyntaxKind.LogicalAndExpression, + result, + conditions[i] + ); + } + + return result; + } } } diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithAndPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithAndPattern.verified.txt new file mode 100644 index 0000000..da5e306 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithAndPattern.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Extensions_IsInRange_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity entity) => entity.Value >= 1 && entity.Value <= 100 ? true : false; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt new file mode 100644 index 0000000..6356921 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Extensions_IsNull_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity entity) => entity == null ? true : false; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt new file mode 100644 index 0000000..797a367 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Extensions_IsNotNull_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity entity) => !(entity == null) ? true : false; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithOrPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithOrPattern.verified.txt new file mode 100644 index 0000000..b5e5f65 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithOrPattern.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Extensions_IsTerminal_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity entity) => entity.Status == "Cancelled" || entity.Status == "Completed" ? true : false; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt new file mode 100644 index 0000000..a11076d --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Extensions_GetComplexCategory_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity entity) => entity != null && entity.IsActive == true && entity.Value > 100 ? "Active High" : "Other"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt new file mode 100644 index 0000000..55dcb0a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Extensions_GetCategory_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity entity) => entity.Value > 100 ? "High" : "Low"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithAndPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithAndPattern.verified.txt new file mode 100644 index 0000000..5ee1641 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithAndPattern.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_IsInRange + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.Value >= 1 && @this.Value <= 100; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithNotNullPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithNotNullPattern.verified.txt new file mode 100644 index 0000000..dba7ad6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithNotNullPattern.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_HasName + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => !(@this.Name == null); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithOrPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithOrPattern.verified.txt new file mode 100644 index 0000000..04942c7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithOrPattern.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_IsOutOfRange + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.Value == 0 || @this.Value > 100; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithPropertyPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithPropertyPattern.verified.txt new file mode 100644 index 0000000..cc2201f --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExpressionBodied_IsPattern_WithPropertyPattern.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Extensions_IsActiveAndPositive_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity entity) => entity != null && entity.IsActive == true && entity.Value > 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.DotNet10_0.verified.txt new file mode 100644 index 0000000..a16c95c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.DotNet10_0.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_IEntityExtensions_Label + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.IEntity @this) => @this.Id + ": " + @this.Name; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.verified.txt new file mode 100644 index 0000000..a16c95c --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberOnInterface.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_IEntityExtensions_Label + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.IEntity @this) => @this.Id + ": " + @this.Name; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.DotNet10_0.verified.txt new file mode 100644 index 0000000..70c9c10 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.DotNet10_0.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_EntityExtensions_GetStatus_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.IsActive && @this.Value > 0 ? "Active" : "Inactive"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.verified.txt new file mode 100644 index 0000000..70c9c10 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithBlockBody.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_EntityExtensions_GetStatus_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.IsActive && @this.Value > 0 ? "Active" : "Inactive"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.DotNet10_0.verified.txt new file mode 100644 index 0000000..758dab3 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.DotNet10_0.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_EntityExtensions_IsHighValue + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.Value > 100; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.verified.txt new file mode 100644 index 0000000..758dab3 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithIsPatternExpression.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_EntityExtensions_IsHighValue + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.Value > 100; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.DotNet10_0.verified.txt new file mode 100644 index 0000000..2bbeb71 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.DotNet10_0.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_EntityExtensions_GetGrade_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.Score >= 90 ? "A" : @this.Score >= 80 ? "B" : @this.Score >= 70 ? "C" : "F"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.verified.txt new file mode 100644 index 0000000..2bbeb71 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ExtensionMemberWithSwitchExpression.verified.txt @@ -0,0 +1,17 @@ +// +#nullable disable +using System; +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_EntityExtensions_GetGrade_P0_Foo_Entity + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.Score >= 90 ? "A" : @this.Score >= 80 ? "B" : @this.Score >= 70 ? "C" : "F"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpressionWithTypePattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpressionWithTypePattern.verified.txt index c9efd3c..e611e59 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpressionWithTypePattern.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpressionWithTypePattern.verified.txt @@ -9,7 +9,7 @@ namespace EntityFrameworkCore.Projectables.Generated { static global::System.Linq.Expressions.Expression> Expression() { - return (global::Item item) => item.GetType() == typeof(GroupItem) ? new global::GroupData(((GroupItem)item).Id, ((GroupItem)item).Name, ((GroupItem)item).Description) : item.GetType() == typeof(DocumentItem) ? new global::DocumentData(((DocumentItem)item).Id, ((DocumentItem)item).Name, ((DocumentItem)item).Priority) : null !; + return (global::Item item) => item is GroupItem ? new global::GroupData(((GroupItem)item).Id, ((GroupItem)item).Name, ((GroupItem)item).Description) : item is DocumentItem ? new global::DocumentData(((DocumentItem)item).Id, ((DocumentItem)item).Name, ((DocumentItem)item).Priority) : null !; } } } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern.verified.txt new file mode 100644 index 0000000..6b1ef59 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_Entity_GetGrade + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Entity @this) => @this.Score >= 90 ? "A" : @this.Score >= 80 ? "B" : @this.Score >= 70 ? "C" : "F"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern_OnExtensionMethod.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern_OnExtensionMethod.verified.txt new file mode 100644 index 0000000..2de6000 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.SwitchExpression_WithRelationalPattern_OnExtensionMethod.verified.txt @@ -0,0 +1,16 @@ +// +#nullable disable +using EntityFrameworkCore.Projectables; +using Foo; + +namespace EntityFrameworkCore.Projectables.Generated +{ + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + static class Foo_OrderExtensions_GetTier_P0_Foo_Order + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.Order order) => order.Amount >= 1000 ? "Platinum" : order.Amount >= 500 ? "Gold" : order.Amount >= 100 ? "Silver" : "Bronze"; + } + } +} \ 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 9ca78b1..57c06cf 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2011,6 +2011,236 @@ public static ItemData ToData(this Item item) => return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task SwitchExpression_WithRelationalPattern() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public int Score { get; set; } + + [Projectable] + public string GetGrade() => Score switch + { + >= 90 => ""A"", + >= 80 => ""B"", + >= 70 => ""C"", + _ => ""F"", + }; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task SwitchExpression_WithRelationalPattern_OnExtensionMethod() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Order { + public decimal Amount { get; set; } + } + + static class OrderExtensions { + [Projectable] + public static string GetTier(this Order order) => order.Amount switch + { + >= 1000 => ""Platinum"", + >= 500 => ""Gold"", + >= 100 => ""Silver"", + _ => ""Bronze"", + }; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpressionBodied_IsPattern_WithAndPattern() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public int Value { get; set; } + + [Projectable] + public bool IsInRange => Value is >= 1 and <= 100; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpressionBodied_IsPattern_WithOrPattern() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public int Value { get; set; } + + [Projectable] + public bool IsOutOfRange => Value is 0 or > 100; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpressionBodied_IsPattern_WithPropertyPattern() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public bool IsActive { get; set; } + public int Value { get; set; } + } + + static class Extensions { + [Projectable] + public static bool IsActiveAndPositive(this Entity entity) => + entity is { IsActive: true, Value: > 0 }; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExpressionBodied_IsPattern_WithNotNullPattern() + { + var compilation = CreateCompilation(@" +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public string? Name { get; set; } + + [Projectable] + public bool HasName => Name is not null; + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithAndPattern() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public int Value { get; set; } + } + + static class Extensions { + [Projectable(AllowBlockBody = true)] + public static bool IsInRange(this Entity entity) + { + if (entity.Value is >= 1 and <= 100) + { + return true; + } + return false; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithOrPattern() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public string Status { get; set; } + } + + static class Extensions { + [Projectable(AllowBlockBody = true)] + public static bool IsTerminal(this Entity entity) + { + if (entity.Status is ""Cancelled"" or ""Completed"") + { + return true; + } + return false; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + [Fact] public Task GenericTypes() { @@ -2967,6 +3197,137 @@ static class EntityExtensions { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + + [Fact] + public Task ExtensionMemberWithBlockBody() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public int Value { get; set; } + public bool IsActive { get; set; } + } + + static class EntityExtensions { + extension(Entity e) { + [Projectable(AllowBlockBody = true)] + public string GetStatus() + { + if (e.IsActive && e.Value > 0) + { + return ""Active""; + } + return ""Inactive""; + } + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExtensionMemberWithSwitchExpression() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public int Score { get; set; } + } + + static class EntityExtensions { + extension(Entity e) { + [Projectable] + public string GetGrade() => e.Score switch + { + >= 90 => ""A"", + >= 80 => ""B"", + >= 70 => ""C"", + _ => ""F"", + }; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExtensionMemberOnInterface() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + interface IEntity { + int Id { get; } + string Name { get; } + } + + static class IEntityExtensions { + extension(IEntity e) { + [Projectable] + public string Label => e.Id + "": "" + e.Name; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ExtensionMemberWithIsPatternExpression() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class Entity { + public int Value { get; set; } + } + + static class EntityExtensions { + extension(Entity e) { + [Projectable] + public bool IsHighValue => e.Value is > 100; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } #endif [Fact] @@ -3506,6 +3867,140 @@ public int GetDouble() // Should have no warnings Assert.Empty(result.Diagnostics); } + + [Fact] + public Task BlockBodiedMethod_WithPatternMatching() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class Entity { + public bool IsActive { get; set; } + public int Value { get; set; } + } + + static class Extensions { + [Projectable(AllowBlockBody = true)] + public static string GetComplexCategory(this Entity entity) + { + if (entity is { IsActive: true, Value: > 100 }) + { + return ""Active High""; + } + return ""Other""; + } + } +} +"); + + var result = RunGenerator(compilation); + + // The generator should not crash and should handle pattern matching + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithRelationalPattern() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class Entity { + public int Value { get; set; } + } + + static class Extensions { + [Projectable(AllowBlockBody = true)] + public static string GetCategory(this Entity entity) + { + if (entity.Value is > 100) + { + return ""High""; + } + return ""Low""; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithConstantPattern() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class Entity { + public string Status { get; set; } + } + + static class Extensions { + [Projectable(AllowBlockBody = true)] + public static bool IsNull(this Entity entity) + { + if (entity is null) + { + return true; + } + return false; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithNotPattern() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class Entity { + public string Name { get; set; } + } + + static class Extensions { + [Projectable(AllowBlockBody = true)] + public static bool IsNotNull(this Entity entity) + { + if (entity is not null) + { + return true; + } + return false; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } #region Helpers