From ef840f69385b4da55e0b43c31488be8bcfa47bff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:26:52 +0000 Subject: [PATCH 01/26] Initial plan From 8468dc516a7faee3e87eaf4974abbc00108224a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:36:26 +0000 Subject: [PATCH 02/26] Add support for block-bodied methods with common statements - Created BlockStatementConverter to transform block bodies to expressions - Added support for simple return statements - Added support for if-else statements (converted to ternary) - Added support for local variable declarations (inlined) - Added diagnostics for unsupported statements (EFP0003) - Added comprehensive test cases - Updated existing test that expected block methods to fail Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- .../AnalyzerReleases.Unshipped.md | 1 + .../BlockStatementConverter.cs | 222 ++++++++++++++++ .../Diagnostics.cs | 8 + .../ProjectableInterpreter.cs | 30 ++- ...lockBodiedMethod_SimpleReturn.verified.txt | 17 ++ ...upportedStatement_WithoutElse.verified.txt | 3 + ....BlockBodiedMethod_WithIfElse.verified.txt | 17 ++ ...Method_WithIfElseAndCondition.verified.txt | 17 ++ ...odiedMethod_WithLocalVariable.verified.txt | 17 ++ ...Method_WithMultipleParameters.verified.txt | 17 ++ ...BodiedMethod_WithNestedIfElse.verified.txt | 17 ++ ...diedMethod_WithPropertyAccess.verified.txt | 17 ++ .../ProjectionExpressionGeneratorTests.cs | 250 +++++++++++++++++- 13 files changed, 628 insertions(+), 5 deletions(-) create mode 100644 src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md index ef168b8..4911eaa 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md +++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md @@ -3,3 +3,4 @@ Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- EFP0002 | Design | Error | +EFP0003 | Design | Warning | diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs new file mode 100644 index 0000000..7c000af --- /dev/null +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -0,0 +1,222 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Linq; + +namespace EntityFrameworkCore.Projectables.Generator +{ + /// + /// Converts block-bodied methods to expression syntax that can be used in expression trees. + /// Only supports a subset of C# statements. + /// + public class BlockStatementConverter + { + private readonly SemanticModel _semanticModel; + private readonly SourceProductionContext _context; + private readonly ExpressionSyntaxRewriter _expressionRewriter; + private readonly Dictionary _localVariables = new(); + + public BlockStatementConverter(SemanticModel semanticModel, SourceProductionContext context, ExpressionSyntaxRewriter expressionRewriter) + { + _semanticModel = semanticModel; + _context = context; + _expressionRewriter = expressionRewriter; + } + + /// + /// Attempts to convert a block statement into a single expression. + /// Returns null if the block contains unsupported statements. + /// + public ExpressionSyntax? TryConvertBlock(BlockSyntax block, string memberName) + { + if (block == null || block.Statements.Count == 0) + { + return null; + } + + // Try to convert the block statements into an expression + var result = TryConvertStatements(block.Statements.ToList(), memberName); + return result; + } + + private ExpressionSyntax? TryConvertStatements(List statements, string memberName) + { + if (statements.Count == 0) + { + return null; + } + + if (statements.Count == 1) + { + return TryConvertStatement(statements[0], memberName); + } + + // Multiple statements - try to convert them into a chain of expressions + // This is done by converting local variable declarations and then the final return + var nonReturnStatements = statements.Take(statements.Count - 1).ToList(); + var lastStatement = statements.Last(); + + // Process local variable declarations + foreach (var stmt in nonReturnStatements) + { + if (stmt is LocalDeclarationStatementSyntax localDecl) + { + if (!TryProcessLocalDeclaration(localDecl, memberName)) + { + return null; + } + } + else + { + ReportUnsupportedStatement(stmt, memberName, "Only local variable declarations are supported before the return statement"); + return null; + } + } + + // Convert the final statement (should be a return) + return TryConvertStatement(lastStatement, memberName); + } + + private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDecl, string memberName) + { + foreach (var variable in localDecl.Declaration.Variables) + { + if (variable.Initializer == null) + { + ReportUnsupportedStatement(localDecl, memberName, "Local variables must have an initializer"); + return false; + } + + var variableName = variable.Identifier.Text; + // Rewrite the initializer expression NOW while it's still in the tree + var rewrittenInitializer = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value); + _localVariables[variableName] = rewrittenInitializer; + } + + return true; + } + + private ExpressionSyntax? TryConvertStatement(StatementSyntax statement, string memberName) + { + switch (statement) + { + case ReturnStatementSyntax returnStmt: + return TryConvertReturnStatement(returnStmt, memberName); + + case IfStatementSyntax ifStmt: + return TryConvertIfStatement(ifStmt, memberName); + + case BlockSyntax blockStmt: + return TryConvertStatements(blockStmt.Statements.ToList(), memberName); + + case ExpressionStatementSyntax exprStmt: + // Expression statements are generally not useful in expression trees + ReportUnsupportedStatement(statement, memberName, "Expression statements are not supported"); + return null; + + case LocalDeclarationStatementSyntax: + // Local declarations should be handled before the return statement + ReportUnsupportedStatement(statement, memberName, "Local declarations must appear before the return statement"); + return null; + + default: + ReportUnsupportedStatement(statement, memberName, $"Statement type '{statement.GetType().Name}' is not supported"); + return null; + } + } + + private ExpressionSyntax? TryConvertReturnStatement(ReturnStatementSyntax returnStmt, string memberName) + { + if (returnStmt.Expression == null) + { + ReportUnsupportedStatement(returnStmt, memberName, "Return statement must have an expression"); + return null; + } + + // First rewrite the return expression + var expression = (ExpressionSyntax)_expressionRewriter.Visit(returnStmt.Expression); + + // Then replace any local variable references with their already-rewritten initializers + expression = ReplaceLocalVariables(expression); + + return expression; + } + + private ExpressionSyntax? TryConvertIfStatement(IfStatementSyntax ifStmt, string memberName) + { + // Convert if-else to conditional (ternary) expression + // First, rewrite the condition using the expression rewriter + var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifStmt.Condition); + + var whenTrue = TryConvertStatement(ifStmt.Statement, memberName); + if (whenTrue == null) + { + return null; + } + + ExpressionSyntax? whenFalse; + if (ifStmt.Else != null) + { + whenFalse = TryConvertStatement(ifStmt.Else.Statement, memberName); + if (whenFalse == null) + { + return null; + } + } + else + { + // If there's no else clause, we can't convert to a ternary + ReportUnsupportedStatement(ifStmt, memberName, "If statements must have an else clause to be converted to expressions"); + return null; + } + + // Create a conditional expression with the rewritten nodes + return SyntaxFactory.ConditionalExpression( + condition, + whenTrue, + whenFalse + ); + } + + private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) + { + // Use a rewriter to replace local variable references with their initializer expressions + var rewriter = new LocalVariableReplacer(_localVariables); + return (ExpressionSyntax)rewriter.Visit(expression); + } + + private void ReportUnsupportedStatement(StatementSyntax statement, string memberName, string reason) + { + var diagnostic = Diagnostic.Create( + Diagnostics.UnsupportedStatementInBlockBody, + statement.GetLocation(), + memberName, + reason + ); + _context.ReportDiagnostic(diagnostic); + } + + private class LocalVariableReplacer : CSharpSyntaxRewriter + { + private readonly Dictionary _localVariables; + + public LocalVariableReplacer(Dictionary localVariables) + { + _localVariables = localVariables; + } + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + { + var identifier = node.Identifier.Text; + if (_localVariables.TryGetValue(identifier, out var replacement)) + { + // Replace the identifier with the expression it was initialized with + return replacement.WithTriviaFrom(node); + } + + return base.VisitIdentifierName(node); + } + } + } +} diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs index 18f87c1..d98a1b8 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs @@ -25,5 +25,13 @@ public static class Diagnostics DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor UnsupportedStatementInBlockBody = new DiagnosticDescriptor( + id: "EFP0003", + title: "Unsupported statement in block-bodied method", + messageFormat: "Method '{0}' contains an unsupported statement: {1}", + category: "Design", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs index 339b46b..36bd51c 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs @@ -97,7 +97,7 @@ x is IPropertySymbol xProperty && return false; } else if (x is MethodDeclarationSyntax xMethod && - xMethod.ExpressionBody is not null) + (xMethod.ExpressionBody is not null || xMethod.Body is not null)) { return true; } @@ -212,7 +212,28 @@ x is IPropertySymbol xProperty && if (memberBody is MethodDeclarationSyntax methodDeclarationSyntax) { - if (methodDeclarationSyntax.ExpressionBody is null) + ExpressionSyntax? bodyExpression = null; + + if (methodDeclarationSyntax.ExpressionBody is not null) + { + // Expression-bodied method (e.g., int Foo() => 1;) + bodyExpression = methodDeclarationSyntax.ExpressionBody.Expression; + } + else if (methodDeclarationSyntax.Body is not null) + { + // Block-bodied method (e.g., int Foo() { return 1; }) + var blockConverter = new BlockStatementConverter(semanticModel, context, expressionSyntaxRewriter); + bodyExpression = blockConverter.TryConvertBlock(methodDeclarationSyntax.Body, memberSymbol.Name); + + if (bodyExpression is null) + { + // Diagnostics already reported by BlockStatementConverter + return null; + } + + // The expression has already been rewritten by BlockStatementConverter, so we don't rewrite it again + } + else { var diagnostic = Diagnostic.Create(Diagnostics.RequiresExpressionBodyDefinition, methodDeclarationSyntax.GetLocation(), memberSymbol.Name); context.ReportDiagnostic(diagnostic); @@ -222,7 +243,10 @@ x is IPropertySymbol xProperty && var returnType = declarationSyntaxRewriter.Visit(methodDeclarationSyntax.ReturnType); descriptor.ReturnTypeName = returnType.ToString(); - descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(methodDeclarationSyntax.ExpressionBody.Expression); + // Only rewrite expression-bodied methods, block-bodied methods are already rewritten + descriptor.ExpressionBody = methodDeclarationSyntax.ExpressionBody is not null + ? (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression) + : bodyExpression; foreach (var additionalParameter in ((ParameterListSyntax)declarationSyntaxRewriter.Visit(methodDeclarationSyntax.ParameterList)).Parameters) { descriptor.ParametersList = descriptor.ParametersList.AddParameters(additionalParameter); diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.verified.txt new file mode 100644 index 0000000..eeb0754 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SimpleReturn.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => 42; + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt new file mode 100644 index 0000000..ed766a2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt @@ -0,0 +1,3 @@ +[ + (11,13): warning EFP0003: Method 'Foo' contains an unsupported statement: Only local variable declarations are supported before the return statement +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.verified.txt new file mode 100644 index 0000000..c22d885 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElse.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar > 10 ? 1 : 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.verified.txt new file mode 100644 index 0000000..ef8f31a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithIfElseAndCondition.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.IsActive && @this.Bar > 0 ? @this.Bar * 2 : 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt new file mode 100644 index 0000000..d863659 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar * 2 + 5; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt new file mode 100644 index 0000000..c454e34 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.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_C_Add + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this, int a, int b) => a + b; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.verified.txt new file mode 100644 index 0000000..216b8f2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNestedIfElse.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar > 10 ? "High" : @this.Bar > 5 ? "Medium" : "Low"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.verified.txt new file mode 100644 index 0000000..19e29c9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPropertyAccess.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar + 10; + } + } +} \ 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 eb50a78..c408db9 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -493,7 +493,7 @@ public int Foo } [Fact] - public void BlockBodiedMethod_RaisesDiagnostics() + public void BlockBodiedMethod_NoLongerRaisesDiagnostics() { var compilation = CreateCompilation(@" using System; @@ -511,7 +511,9 @@ public int Foo() var result = RunGenerator(compilation); - Assert.Single(result.Diagnostics); + // Block-bodied methods are now supported, so no diagnostics should be raised + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); } [Fact] @@ -1977,6 +1979,250 @@ public static Dictionary ToDictionary(this Entity entity) return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task BlockBodiedMethod_SimpleReturn() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + [Projectable] + public int Foo() + { + return 42; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithPropertyAccess() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + return Bar + 10; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithIfElse() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + if (Bar > 10) + { + return 1; + } + else + { + return 0; + } + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithNestedIfElse() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public string Foo() + { + if (Bar > 10) + { + return ""High""; + } + else if (Bar > 5) + { + return ""Medium""; + } + else + { + 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_WithLocalVariable() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + var temp = Bar * 2; + return temp + 5; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithMultipleParameters() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + [Projectable] + public int Add(int a, int b) + { + return a + b; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_WithIfElseAndCondition() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + public bool IsActive { get; set; } + + [Projectable] + public int Foo() + { + if (IsActive && Bar > 0) + { + return Bar * 2; + } + else + { + return 0; + } + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_UnsupportedStatement_WithoutElse() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + if (Bar > 10) + { + return 1; + } + return 0; + } + } +} +", expectedToCompile: true); + + var result = RunGenerator(compilation); + + // Should have a warning diagnostic + Assert.NotEmpty(result.Diagnostics); + Assert.Contains(result.Diagnostics, d => d.Id == "EFP0003"); + + return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); + } + #region Helpers Compilation CreateCompilation(string source, bool expectedToCompile = true) From f68c572e7eff047ee6dd4e4a04c491d054088a9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:40:27 +0000 Subject: [PATCH 03/26] Add functional tests and documentation for block-bodied methods - Created 7 functional tests demonstrating EF Core SQL translation - Added comprehensive documentation explaining feature, limitations, and benefits - All 174 tests passing across all projects Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- docs/BlockBodiedMethods.md | 159 ++++++++++++++++ ...ers_WorksCorrectly.DotNet10_0.verified.txt | 2 + ...ters_WorksCorrectly.DotNet9_0.verified.txt | 2 + ...WithParameters_WorksCorrectly.verified.txt | 2 + ...ranslatedCorrectly.DotNet10_0.verified.txt | 5 + ...TranslatedCorrectly.DotNet9_0.verified.txt | 5 + ...itional_IsTranslatedCorrectly.verified.txt | 5 + ...ranslatedToTernary.DotNet10_0.verified.txt | 5 + ...TranslatedToTernary.DotNet9_0.verified.txt | 5 + ...atement_IsTranslatedToTernary.verified.txt | 5 + ...Variable_IsInlined.DotNet10_0.verified.txt | 2 + ...lVariable_IsInlined.DotNet9_0.verified.txt | 2 + ...Tests.LocalVariable_IsInlined.verified.txt | 2 + ...tedToNestedTernary.DotNet10_0.verified.txt | 6 + ...atedToNestedTernary.DotNet9_0.verified.txt | 6 + ...e_IsTranslatedToNestedTernary.verified.txt | 6 + ..._IsTranslatedToSql.DotNet10_0.verified.txt | 2 + ...s_IsTranslatedToSql.DotNet9_0.verified.txt | 2 + ...pertyAccess_IsTranslatedToSql.verified.txt | 2 + ..._IsTranslatedToSql.DotNet10_0.verified.txt | 2 + ...n_IsTranslatedToSql.DotNet9_0.verified.txt | 2 + ...impleReturn_IsTranslatedToSql.verified.txt | 2 + .../BlockBodiedMethodTests.cs | 169 ++++++++++++++++++ 23 files changed, 400 insertions(+) create mode 100644 docs/BlockBodiedMethods.md create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md new file mode 100644 index 0000000..a62cace --- /dev/null +++ b/docs/BlockBodiedMethods.md @@ -0,0 +1,159 @@ +# Block-Bodied Methods Support + +As of this version, EntityFrameworkCore.Projectables now supports "classic" block-bodied methods decorated with `[Projectable]`, in addition to expression-bodied methods. + +## What's Supported + +Block-bodied methods can now be transformed into expression trees when they contain: + +### 1. Simple Return Statements +```csharp +[Projectable] +public int GetConstant() +{ + return 42; +} +``` + +### 2. If-Else Statements (converted to ternary expressions) +```csharp +[Projectable] +public string GetCategory() +{ + if (Value > 100) + { + return "High"; + } + else + { + return "Low"; + } +} +``` + +### 3. Nested If-Else Statements +```csharp +[Projectable] +public string GetLevel() +{ + if (Value > 100) + { + return "High"; + } + else if (Value > 50) + { + return "Medium"; + } + else + { + return "Low"; + } +} +``` + +### 4. Local Variable Declarations (inlined into the expression) +```csharp +[Projectable] +public int CalculateDouble() +{ + var doubled = Value * 2; + return doubled + 5; +} +``` + +## Limitations and Warnings + +The source generator will produce **warning EFP0003** when it encounters unsupported statements in block-bodied methods: + +### Unsupported Statements: +- If statements without else clauses +- While, for, foreach loops +- Switch statements (use switch expressions instead) +- Try-catch-finally blocks +- Throw statements +- New object instantiation in statement position +- Multiple statements (except local variable declarations before return) + +### Example of Unsupported Pattern: +```csharp +[Projectable] +public int GetValue() +{ + if (IsActive) // ❌ No else clause - will produce EFP0003 warning + { + return Value; + } + return 0; +} +``` + +Should be written as: +```csharp +[Projectable] +public int GetValue() +{ + if (IsActive) // ✅ Has else clause + { + return Value; + } + else + { + return 0; + } +} +``` + +Or as expression-bodied: +```csharp +[Projectable] +public int GetValue() => IsActive ? Value : 0; // ✅ Expression-bodied +``` + +## How It Works + +The source generator: +1. Parses block-bodied methods +2. Converts if-else statements to conditional (ternary) expressions +3. Inlines local variables into the return expression +4. Rewrites the resulting expression using the existing expression transformation pipeline +5. Generates the same output as expression-bodied methods + +## Benefits + +- **More readable code**: Complex logic with nested conditions is often easier to read with if-else blocks than with nested ternary operators +- **Gradual migration**: Existing code with block bodies can now be marked as `[Projectable]` without rewriting +- **Intermediate variables**: Local variables can make complex calculations more understandable + +## Example Output + +Given this code: +```csharp +public record Entity +{ + public int Value { get; set; } + public bool IsActive { get; set; } + + [Projectable] + public int GetAdjustedValue() + { + if (IsActive && Value > 0) + { + return Value * 2; + } + else + { + return 0; + } + } +} +``` + +The generated SQL will be: +```sql +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0 + THEN [e].[Value] * 2 + ELSE 0 +END +FROM [Entity] AS [e] +``` diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..dab6bd4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT 15 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..dab6bd4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT 15 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt new file mode 100644 index 0000000..dab6bd4 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt @@ -0,0 +1,2 @@ +SELECT 15 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..a19f725 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0 THEN [e].[Value] * 2 + ELSE 0 +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..a19f725 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0 THEN [e].[Value] * 2 + ELSE 0 +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt new file mode 100644 index 0000000..a19f725 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 0 THEN [e].[Value] * 2 + ELSE 0 +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt new file mode 100644 index 0000000..26ac26b --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt new file mode 100644 index 0000000..26ac26b --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt new file mode 100644 index 0000000..26ac26b --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt new file mode 100644 index 0000000..9689484 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 5 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt new file mode 100644 index 0000000..9689484 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 5 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt new file mode 100644 index 0000000..9689484 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 5 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt new file mode 100644 index 0000000..9d42002 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + WHEN [e].[Value] > 50 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt new file mode 100644 index 0000000..9d42002 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + WHEN [e].[Value] > 50 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt new file mode 100644 index 0000000..9d42002 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + WHEN [e].[Value] > 50 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt new file mode 100644 index 0000000..06a56fa --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt new file mode 100644 index 0000000..06a56fa --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt new file mode 100644 index 0000000..06a56fa --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt new file mode 100644 index 0000000..6efc8d2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt new file mode 100644 index 0000000..6efc8d2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt new file mode 100644 index 0000000..6efc8d2 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt @@ -0,0 +1,2 @@ +SELECT 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs new file mode 100644 index 0000000..435c2a6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs @@ -0,0 +1,169 @@ +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System.Threading.Tasks; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.FunctionalTests +{ + [UsesVerify] + public class BlockBodiedMethodTests + { + public record Entity + { + public int Id { get; set; } + public int Value { get; set; } + public bool IsActive { get; set; } + public string? Name { get; set; } + } + + [Fact] + public Task SimpleReturn_IsTranslatedToSql() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetConstant()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task ReturnWithPropertyAccess_IsTranslatedToSql() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetValuePlusTen()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task IfElseStatement_IsTranslatedToTernary() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetCategory()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task NestedIfElse_IsTranslatedToNestedTernary() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetLevel()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task LocalVariable_IsInlined() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.CalculateDouble()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task ComplexConditional_IsTranslatedCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetAdjustedValue()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockMethodWithParameters_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.Add(5, 10)); + + return Verifier.Verify(query.ToQueryString()); + } + } + + public static class EntityExtensions + { + [Projectable] + public static int GetConstant(this BlockBodiedMethodTests.Entity entity) + { + return 42; + } + + [Projectable] + public static int GetValuePlusTen(this BlockBodiedMethodTests.Entity entity) + { + return entity.Value + 10; + } + + [Projectable] + public static string GetCategory(this BlockBodiedMethodTests.Entity entity) + { + if (entity.Value > 100) + { + return "High"; + } + else + { + return "Low"; + } + } + + [Projectable] + public static string GetLevel(this BlockBodiedMethodTests.Entity entity) + { + if (entity.Value > 100) + { + return "High"; + } + else if (entity.Value > 50) + { + return "Medium"; + } + else + { + return "Low"; + } + } + + [Projectable] + public static int CalculateDouble(this BlockBodiedMethodTests.Entity entity) + { + var doubled = entity.Value * 2; + return doubled + 5; + } + + [Projectable] + public static int GetAdjustedValue(this BlockBodiedMethodTests.Entity entity) + { + if (entity.IsActive && entity.Value > 0) + { + return entity.Value * 2; + } + else + { + return 0; + } + } + + [Projectable] + public static int Add(this BlockBodiedMethodTests.Entity entity, int a, int b) + { + return a + b; + } + } +} From 7067bc28c48a95e4068ff23f38bda9e7c1ad5e2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 12:05:34 +0000 Subject: [PATCH 04/26] Add support for if-without-else and switch statements - Support if statements without else clause (uses default or fallback return) - Support switch statements (converted to nested conditionals) - Handle if { return x; } return y; pattern - Added 5 generator tests and 4 functional tests - Updated documentation with new features and SQL examples - All 182 tests passing (84 generator + 76 functional + 22 unit) Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- docs/BlockBodiedMethods.md | 124 +++++++++++-- .../BlockStatementConverter.cs | 164 +++++++++++++++++- ...utElse_UsesDefault.DotNet10_0.verified.txt | 4 + ...outElse_UsesDefault.DotNet9_0.verified.txt | 4 + ...sts.IfWithoutElse_UsesDefault.verified.txt | 5 + ...WithFallbackReturn.DotNet10_0.verified.txt | 5 + ..._WithFallbackReturn.DotNet9_0.verified.txt | 5 + ...ithoutElse_WithFallbackReturn.verified.txt | 5 + ...chStatement_Simple.DotNet10_0.verified.txt | 7 + ...tchStatement_Simple.DotNet9_0.verified.txt | 7 + ...dTests.SwitchStatement_Simple.verified.txt | 7 + ..._WithMultipleCases.DotNet10_0.verified.txt | 7 + ...t_WithMultipleCases.DotNet9_0.verified.txt | 7 + ...chStatement_WithMultipleCases.verified.txt | 7 + .../BlockBodiedMethodTests.cs | 101 +++++++++++ ..._IfWithoutElse_ReturnsDefault.verified.txt | 17 ++ ...hod_IfWithoutElse_UsesDefault.verified.txt | 17 ++ ...Method_SwitchStatement_Simple.verified.txt | 17 ++ ...chStatement_WithMultipleCases.verified.txt | 17 ++ ...witchStatement_WithoutDefault.verified.txt | 17 ++ ...upportedStatement_WithoutElse.verified.txt | 3 - .../ProjectionExpressionGeneratorTests.cs | 148 +++++++++++++++- 22 files changed, 671 insertions(+), 24 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md index a62cace..9dc6e7a 100644 --- a/docs/BlockBodiedMethods.md +++ b/docs/BlockBodiedMethods.md @@ -61,38 +61,78 @@ public int CalculateDouble() } ``` +### 5. Switch Statements (converted to nested ternary expressions) +```csharp +[Projectable] +public string GetValueLabel() +{ + switch (Value) + { + case 1: + return "One"; + case 2: + return "Two"; + case 3: + return "Three"; + default: + return "Many"; + } +} +``` + +### 6. If Statements Without Else (uses default value) +```csharp +[Projectable] +public int? GetPremiumIfActive() +{ + if (IsActive) + { + return Value * 2; + } + // Implicitly returns null (default for int?) +} + +// Or with explicit fallback: +[Projectable] +public string GetStatus() +{ + if (IsActive) + { + return "Active"; + } + return "Inactive"; // Explicit fallback +} +``` + ## Limitations and Warnings The source generator will produce **warning EFP0003** when it encounters unsupported statements in block-bodied methods: ### Unsupported Statements: -- If statements without else clauses - While, for, foreach loops -- Switch statements (use switch expressions instead) - Try-catch-finally blocks - Throw statements - New object instantiation in statement position -- Multiple statements (except local variable declarations before return) ### Example of Unsupported Pattern: ```csharp [Projectable] public int GetValue() { - if (IsActive) // ❌ No else clause - will produce EFP0003 warning + for (int i = 0; i < 10; i++) // ❌ Loops not supported { - return Value; + // ... } return 0; } ``` -Should be written as: +Supported patterns: ```csharp [Projectable] public int GetValue() { - if (IsActive) // ✅ Has else clause + if (IsActive) // ✅ If without else is now supported! { return Value; } @@ -103,6 +143,35 @@ public int GetValue() } ``` +Additional supported patterns: +```csharp +// If without else using fallback return: +[Projectable] +public int GetValue() +{ + if (IsActive) + { + return Value; + } + return 0; // ✅ Fallback return +} + +// Switch statement: +[Projectable] +public string GetLabel() +{ + switch (Value) // ✅ Switch statements now supported! + { + case 1: + return "One"; + case 2: + return "Two"; + default: + return "Other"; + } +} +``` + Or as expression-bodied: ```csharp [Projectable] @@ -114,17 +183,48 @@ public int GetValue() => IsActive ? Value : 0; // ✅ Expression-bodied The source generator: 1. Parses block-bodied methods 2. Converts if-else statements to conditional (ternary) expressions -3. Inlines local variables into the return expression -4. Rewrites the resulting expression using the existing expression transformation pipeline -5. Generates the same output as expression-bodied methods +3. Converts switch statements to nested conditional expressions +4. Inlines local variables into the return expression +5. Rewrites the resulting expression using the existing expression transformation pipeline +6. Generates the same output as expression-bodied methods ## Benefits -- **More readable code**: Complex logic with nested conditions is often easier to read with if-else blocks than with nested ternary operators +- **More readable code**: Complex logic with nested conditions and switch statements is often easier to read than nested ternary operators - **Gradual migration**: Existing code with block bodies can now be marked as `[Projectable]` without rewriting - **Intermediate variables**: Local variables can make complex calculations more understandable +- **Switch support**: Traditional switch statements now work alongside switch expressions + +## SQL Output Examples + +### Switch Statement with Multiple Cases +Given this code: +```csharp +switch (Value) +{ + case 1: + case 2: + return "Low"; + case 3: + case 4: + case 5: + return "Medium"; + default: + return "High"; +} +``` + +Generates optimized SQL: +```sql +SELECT CASE + WHEN [e].[Value] IN (1, 2) THEN N'Low' + WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium' + ELSE N'High' +END +FROM [Entity] AS [e] +``` -## Example Output +### If-Else Example Output Given this code: ```csharp diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index 7c000af..db9170c 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -57,6 +57,31 @@ public BlockStatementConverter(SemanticModel semanticModel, SourceProductionCont var nonReturnStatements = statements.Take(statements.Count - 1).ToList(); var lastStatement = statements.Last(); + // Check if we have a pattern like: if { return x; } return y; + // This can be converted to: condition ? x : y + if (nonReturnStatements.Count == 1 && + nonReturnStatements[0] is IfStatementSyntax ifWithoutElse && + ifWithoutElse.Else == null && + lastStatement is ReturnStatementSyntax finalReturn) + { + // Convert: if (condition) { return x; } return y; + // To: condition ? x : y + var ifBody = TryConvertStatement(ifWithoutElse.Statement, memberName); + if (ifBody == null) + { + return null; + } + + var elseBody = TryConvertReturnStatement(finalReturn, memberName); + if (elseBody == null) + { + return null; + } + + var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifWithoutElse.Condition); + return SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody); + } + // Process local variable declarations foreach (var stmt in nonReturnStatements) { @@ -107,6 +132,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec case IfStatementSyntax ifStmt: return TryConvertIfStatement(ifStmt, memberName); + case SwitchStatementSyntax switchStmt: + return TryConvertSwitchStatement(switchStmt, memberName); + case BlockSyntax blockStmt: return TryConvertStatements(blockStmt.Statements.ToList(), memberName); @@ -166,9 +194,12 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec } else { - // If there's no else clause, we can't convert to a ternary - ReportUnsupportedStatement(ifStmt, memberName, "If statements must have an else clause to be converted to expressions"); - return null; + // If there's no else clause, use a default literal + // This will be inferred to the correct type by the compiler + whenFalse = SyntaxFactory.LiteralExpression( + SyntaxKind.DefaultLiteralExpression, + SyntaxFactory.Token(SyntaxKind.DefaultKeyword) + ); } // Create a conditional expression with the rewritten nodes @@ -179,6 +210,133 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec ); } + private ExpressionSyntax? TryConvertSwitchStatement(SwitchStatementSyntax switchStmt, string memberName) + { + // Convert switch statement to nested conditional expressions + // Process sections in reverse order to build from the default case up + + var switchExpression = (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression); + ExpressionSyntax? currentExpression = null; + + // Find default case first + SwitchSectionSyntax? defaultSection = null; + var nonDefaultSections = new List(); + + foreach (var section in switchStmt.Sections) + { + bool hasDefault = section.Labels.Any(label => label is DefaultSwitchLabelSyntax); + if (hasDefault) + { + defaultSection = section; + } + else + { + nonDefaultSections.Add(section); + } + } + + // Start with default case or null + if (defaultSection != null) + { + currentExpression = ConvertSwitchSection(defaultSection, memberName); + if (currentExpression == null) + { + return null; + } + } + else + { + // No default case - use default literal + currentExpression = SyntaxFactory.LiteralExpression( + SyntaxKind.DefaultLiteralExpression, + SyntaxFactory.Token(SyntaxKind.DefaultKeyword) + ); + } + + // Process non-default sections in reverse order + for (int i = nonDefaultSections.Count - 1; i >= 0; i--) + { + var section = nonDefaultSections[i]; + var sectionExpression = ConvertSwitchSection(section, memberName); + if (sectionExpression == null) + { + return null; + } + + // Build condition for all labels in this section (OR'd together) + ExpressionSyntax? condition = null; + foreach (var label in section.Labels) + { + if (label is CaseSwitchLabelSyntax caseLabel) + { + var labelCondition = SyntaxFactory.BinaryExpression( + SyntaxKind.EqualsExpression, + switchExpression, + (ExpressionSyntax)_expressionRewriter.Visit(caseLabel.Value) + ); + + condition = condition == null + ? labelCondition + : SyntaxFactory.BinaryExpression( + SyntaxKind.LogicalOrExpression, + condition, + labelCondition + ); + } + else if (label is not DefaultSwitchLabelSyntax) + { + // Unsupported label type (e.g., pattern-based switch in older syntax) + ReportUnsupportedStatement(switchStmt, memberName, + $"Switch label type '{label.GetType().Name}' is not supported. Use case labels or switch expressions instead."); + return null; + } + } + + if (condition != null) + { + currentExpression = SyntaxFactory.ConditionalExpression( + condition, + sectionExpression, + currentExpression + ); + } + } + + return currentExpression; + } + + private ExpressionSyntax? ConvertSwitchSection(SwitchSectionSyntax section, string memberName) + { + // Convert the statements in the switch section + // Most switch sections end with break, return, or throw + var statements = section.Statements.ToList(); + + // Remove trailing break statements as they're not needed in expressions + if (statements.Count > 0 && statements.Last() is BreakStatementSyntax) + { + statements = statements.Take(statements.Count - 1).ToList(); + } + + if (statements.Count == 0) + { + // Use the section's first label location for error reporting + var firstLabel = section.Labels.FirstOrDefault(); + if (firstLabel != null) + { + var diagnostic = Diagnostic.Create( + Diagnostics.UnsupportedStatementInBlockBody, + firstLabel.GetLocation(), + memberName, + "Switch section must have at least one statement" + ); + _context.ReportDiagnostic(diagnostic); + } + return null; + } + + return TryConvertStatements(statements, memberName); + } + private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) { // Use a rewriter to replace local variable references with their initializer expressions diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt new file mode 100644 index 0000000..0c5fe1e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt @@ -0,0 +1,4 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN [e].[Value] * 2 +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt new file mode 100644 index 0000000..0c5fe1e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt @@ -0,0 +1,4 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN [e].[Value] * 2 +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt new file mode 100644 index 0000000..7e3c8c6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN [e].[Value] * 2 + ELSE NULL +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt new file mode 100644 index 0000000..f3f5c43 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active' + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt new file mode 100644 index 0000000..f3f5c43 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active' + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt new file mode 100644 index 0000000..f3f5c43 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active' + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt new file mode 100644 index 0000000..9ed7fa8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 THEN N'One' + WHEN [e].[Value] = 2 THEN N'Two' + WHEN [e].[Value] = 3 THEN N'Three' + ELSE N'Many' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt new file mode 100644 index 0000000..9ed7fa8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 THEN N'One' + WHEN [e].[Value] = 2 THEN N'Two' + WHEN [e].[Value] = 3 THEN N'Three' + ELSE N'Many' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt new file mode 100644 index 0000000..9ed7fa8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 THEN N'One' + WHEN [e].[Value] = 2 THEN N'Two' + WHEN [e].[Value] = 3 THEN N'Three' + ELSE N'Many' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt new file mode 100644 index 0000000..9c8b78e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] IN (1, 2) THEN N'Low' + WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium' + WHEN [e].[Value] IN (6, 7, 8) THEN N'High' + ELSE N'Critical' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt new file mode 100644 index 0000000..9c8b78e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] IN (1, 2) THEN N'Low' + WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium' + WHEN [e].[Value] IN (6, 7, 8) THEN N'High' + ELSE N'Critical' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt new file mode 100644 index 0000000..9c8b78e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] IN (1, 2) THEN N'Low' + WHEN [e].[Value] IN (3, 4, 5) THEN N'Medium' + WHEN [e].[Value] IN (6, 7, 8) THEN N'High' + ELSE N'Critical' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs index 435c2a6..71ae4d9 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs @@ -94,6 +94,50 @@ public Task BlockMethodWithParameters_WorksCorrectly() return Verifier.Verify(query.ToQueryString()); } + + [Fact] + public Task IfWithoutElse_UsesDefault() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetPremiumIfActive()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task IfWithoutElse_WithFallbackReturn() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetStatus()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SwitchStatement_Simple() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetValueLabel()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SwitchStatement_WithMultipleCases() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetPriority()); + + return Verifier.Verify(query.ToQueryString()); + } } public static class EntityExtensions @@ -165,5 +209,62 @@ public static int Add(this BlockBodiedMethodTests.Entity entity, int a, int b) { return a + b; } + + [Projectable] + public static int? GetPremiumIfActive(this BlockBodiedMethodTests.Entity entity) + { + if (entity.IsActive) + { + return entity.Value * 2; + } + return null; + } + + [Projectable] + public static string GetStatus(this BlockBodiedMethodTests.Entity entity) + { + if (entity.IsActive) + { + return "Active"; + } + return "Inactive"; + } + + [Projectable] + public static string GetValueLabel(this BlockBodiedMethodTests.Entity entity) + { + switch (entity.Value) + { + case 1: + return "One"; + case 2: + return "Two"; + case 3: + return "Three"; + default: + return "Many"; + } + } + + [Projectable] + public static string GetPriority(this BlockBodiedMethodTests.Entity entity) + { + switch (entity.Value) + { + case 1: + case 2: + return "Low"; + case 3: + case 4: + case 5: + return "Medium"; + case 6: + case 7: + case 8: + return "High"; + default: + return "Critical"; + } + } } } diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt new file mode 100644 index 0000000..b5f9f5b --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar > 10 ? 1 : default; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.verified.txt new file mode 100644 index 0000000..c22d885 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_UsesDefault.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar > 10 ? 1 : 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.verified.txt new file mode 100644 index 0000000..d1a7eb5 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_Simple.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar == 1 ? "One" : @this.Bar == 2 ? "Two" : "Other"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.verified.txt new file mode 100644 index 0000000..c90d6b7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithMultipleCases.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar == 1 || @this.Bar == 2 ? "Low" : @this.Bar == 3 || @this.Bar == 4 || @this.Bar == 5 ? "Medium" : "High"; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.verified.txt new file mode 100644 index 0000000..0a4d15d --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_SwitchStatement_WithoutDefault.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar == 1 ? "One" : @this.Bar == 2 ? "Two" : default; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt deleted file mode 100644 index ed766a2..0000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_UnsupportedStatement_WithoutElse.verified.txt +++ /dev/null @@ -1,3 +0,0 @@ -[ - (11,13): warning EFP0003: Method 'Foo' contains an unsupported statement: Only local variable declarations are supported before the return statement -] \ 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 c408db9..8baad79 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2191,8 +2191,9 @@ public int Foo() return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] - public Task BlockBodiedMethod_UnsupportedStatement_WithoutElse() + public Task BlockBodiedMethod_IfWithoutElse_UsesDefault() { var compilation = CreateCompilation(@" using System; @@ -2212,15 +2213,150 @@ public int Foo() } } } -", expectedToCompile: true); +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_IfWithoutElse_ReturnsDefault() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int? Foo() + { + if (Bar > 10) + { + return 1; + } + } + } +} +", expectedToCompile: false); var result = RunGenerator(compilation); - // Should have a warning diagnostic - Assert.NotEmpty(result.Diagnostics); - Assert.Contains(result.Diagnostics, d => d.Id == "EFP0003"); + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_SwitchStatement_Simple() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public string Foo() + { + switch (Bar) + { + case 1: + return ""One""; + case 2: + return ""Two""; + default: + return ""Other""; + } + } + } +} +"); + + var result = RunGenerator(compilation); - return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_SwitchStatement_WithMultipleCases() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public string Foo() + { + switch (Bar) + { + case 1: + case 2: + return ""Low""; + case 3: + case 4: + case 5: + return ""Medium""; + default: + return ""High""; + } + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_SwitchStatement_WithoutDefault() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public string? Foo() + { + switch (Bar) + { + case 1: + return ""One""; + case 2: + return ""Two""; + } + } + } +} +", expectedToCompile: false); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); } #region Helpers From 75c638e3d0d2681fa9a03459f90c471ed15f1d78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 12:08:51 +0000 Subject: [PATCH 05/26] Address code review feedback - Rename test to better reflect implicit return pattern - Add clarifying comment about control flow in BlockStatementConverter - All tests still passing Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- .../BlockStatementConverter.cs | 3 ++- ...lockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt} | 0 .../ProjectionExpressionGeneratorTests.cs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename tests/EntityFrameworkCore.Projectables.Generator.Tests/{ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt => ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt} (100%) diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index db9170c..59ede35 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -82,7 +82,8 @@ nonReturnStatements[0] is IfStatementSyntax ifWithoutElse && return SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody); } - // Process local variable declarations + // If we reach here, the pattern was not detected + // Process local variable declarations before the final return foreach (var stmt in nonReturnStatements) { if (stmt is LocalDeclarationStatementSyntax localDecl) diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ReturnsDefault.verified.txt rename to tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IfWithoutElse_ImplicitReturn.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index 8baad79..9e2d34a 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2224,7 +2224,7 @@ public int Foo() } [Fact] - public Task BlockBodiedMethod_IfWithoutElse_ReturnsDefault() + public Task BlockBodiedMethod_IfWithoutElse_ImplicitReturn() { var compilation = CreateCompilation(@" using System; From 06627c292ceea0ad05389a1cc5ae3495c9bf8ed8 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sun, 15 Feb 2026 10:04:14 +0100 Subject: [PATCH 06/26] Remove unused code and add support for multiple early returns --- .../BlockStatementConverter.cs | 66 +++++++++++++------ .../ProjectableInterpreter.cs | 2 +- .../BlockBodiedMethodTests.cs | 32 +++++++++ ...Method_WithMultipleParameters.verified.txt | 2 +- 4 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index 59ede35..d5817e2 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -1,8 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Collections.Generic; -using System.Linq; namespace EntityFrameworkCore.Projectables.Generator { @@ -12,14 +10,12 @@ namespace EntityFrameworkCore.Projectables.Generator /// public class BlockStatementConverter { - private readonly SemanticModel _semanticModel; private readonly SourceProductionContext _context; private readonly ExpressionSyntaxRewriter _expressionRewriter; private readonly Dictionary _localVariables = new(); - public BlockStatementConverter(SemanticModel semanticModel, SourceProductionContext context, ExpressionSyntaxRewriter expressionRewriter) + public BlockStatementConverter(SourceProductionContext context, ExpressionSyntaxRewriter expressionRewriter) { - _semanticModel = semanticModel; _context = context; _expressionRewriter = expressionRewriter; } @@ -30,7 +26,7 @@ public BlockStatementConverter(SemanticModel semanticModel, SourceProductionCont /// public ExpressionSyntax? TryConvertBlock(BlockSyntax block, string memberName) { - if (block == null || block.Statements.Count == 0) + if (block.Statements.Count == 0) { return null; } @@ -57,12 +53,44 @@ public BlockStatementConverter(SemanticModel semanticModel, SourceProductionCont var nonReturnStatements = statements.Take(statements.Count - 1).ToList(); var lastStatement = statements.Last(); - // Check if we have a pattern like: if { return x; } return y; - // This can be converted to: condition ? x : y - if (nonReturnStatements.Count == 1 && - nonReturnStatements[0] is IfStatementSyntax ifWithoutElse && - ifWithoutElse.Else == null && - lastStatement is ReturnStatementSyntax finalReturn) + // Check if we have a pattern like multiple if statements without else followed by a final return: + // if (a) return 1; if (b) return 2; return 3; + // This can be converted to nested ternaries: a ? 1 : (b ? 2 : 3) + if (lastStatement is ReturnStatementSyntax finalReturn && + nonReturnStatements.All(s => s is IfStatementSyntax { Else: null })) + { + // All non-return statements are if statements without else + var ifStatements = nonReturnStatements.Cast().ToList(); + + // Start with the final return as the base expression + var elseBody = TryConvertReturnStatement(finalReturn, memberName); + if (elseBody == null) + { + return null; + } + + // Build nested conditionals from right to left (last to first) + for (var i = ifStatements.Count - 1; i >= 0; i--) + { + var ifStmt = ifStatements[i]; + var ifBody = TryConvertStatement(ifStmt.Statement, memberName); + if (ifBody == null) + { + return null; + } + + var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifStmt.Condition); + elseBody = SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody); + } + + return elseBody; + } + + // Check if we have a single if without else followed by a return (legacy path) + // This is now redundant with the above logic but kept for clarity and potential optimization + if (nonReturnStatements.Count == 1 && + nonReturnStatements[0] is IfStatementSyntax { Else: null } ifWithoutElse && + lastStatement is ReturnStatementSyntax singleFinalReturn) { // Convert: if (condition) { return x; } return y; // To: condition ? x : y @@ -72,7 +100,7 @@ nonReturnStatements[0] is IfStatementSyntax ifWithoutElse && return null; } - var elseBody = TryConvertReturnStatement(finalReturn, memberName); + var elseBody = TryConvertReturnStatement(singleFinalReturn, memberName); if (elseBody == null) { return null; @@ -115,9 +143,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec } var variableName = variable.Identifier.Text; + // Rewrite the initializer expression NOW while it's still in the tree - var rewrittenInitializer = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value); - _localVariables[variableName] = rewrittenInitializer; + _localVariables[variableName] = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value); } return true; @@ -139,7 +167,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec case BlockSyntax blockStmt: return TryConvertStatements(blockStmt.Statements.ToList(), memberName); - case ExpressionStatementSyntax exprStmt: + case ExpressionStatementSyntax: // Expression statements are generally not useful in expression trees ReportUnsupportedStatement(statement, memberName, "Expression statements are not supported"); return null; @@ -217,7 +245,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec // Process sections in reverse order to build from the default case up var switchExpression = (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression); - ExpressionSyntax? currentExpression = null; + ExpressionSyntax? currentExpression; // Find default case first SwitchSectionSyntax? defaultSection = null; @@ -225,7 +253,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec foreach (var section in switchStmt.Sections) { - bool hasDefault = section.Labels.Any(label => label is DefaultSwitchLabelSyntax); + var hasDefault = section.Labels.Any(label => label is DefaultSwitchLabelSyntax); if (hasDefault) { defaultSection = section; @@ -255,7 +283,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec } // Process non-default sections in reverse order - for (int i = nonDefaultSections.Count - 1; i >= 0; i--) + for (var i = nonDefaultSections.Count - 1; i >= 0; i--) { var section = nonDefaultSections[i]; var sectionExpression = ConvertSwitchSection(section, memberName); diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs index 7bca8cc..5006081 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs @@ -312,7 +312,7 @@ x is IPropertySymbol xProperty && else if (methodDeclarationSyntax.Body is not null) { // Block-bodied method (e.g., int Foo() { return 1; }) - var blockConverter = new BlockStatementConverter(semanticModel, context, expressionSyntaxRewriter); + var blockConverter = new BlockStatementConverter(context, expressionSyntaxRewriter); bodyExpression = blockConverter.TryConvertBlock(methodDeclarationSyntax.Body, memberSymbol.Name); if (bodyExpression is null) diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs index 71ae4d9..a961442 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs @@ -138,6 +138,17 @@ public Task SwitchStatement_WithMultipleCases() return Verifier.Verify(query.ToQueryString()); } + + [Fact] + public Task MultipleEarlyReturns_ConvertedToNestedTernaries() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetValueCategory()); + + return Verifier.Verify(query.ToQueryString()); + } } public static class EntityExtensions @@ -266,5 +277,26 @@ public static string GetPriority(this BlockBodiedMethodTests.Entity entity) return "Critical"; } } + + [Projectable] + public static string GetValueCategory(this BlockBodiedMethodTests.Entity entity) + { + if (entity.Value > 100) + { + return "Very High"; + } + + if (entity.Value > 50) + { + return "High"; + } + + if (entity.Value > 10) + { + return "Medium"; + } + + return "Low"; + } } } diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt index c454e34..7c1426a 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithMultipleParameters.verified.txt @@ -7,7 +7,7 @@ using Foo; namespace EntityFrameworkCore.Projectables.Generated { [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - static class Foo_C_Add + static class Foo_C_Add_P0_int_P1_int { static global::System.Linq.Expressions.Expression> Expression() { From 9add2c922dd3af070f451632bc6b1531cc19d8cf Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sun, 15 Feb 2026 10:08:46 +0100 Subject: [PATCH 07/26] Missing verify files --- ...urns_ConvertedToNestedTernaries.DotNet10_0.verified.txt | 7 +++++++ ...turns_ConvertedToNestedTernaries.DotNet9_0.verified.txt | 7 +++++++ ...pleEarlyReturns_ConvertedToNestedTernaries.verified.txt | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt new file mode 100644 index 0000000..1ae6355 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'Very High' + WHEN [e].[Value] > 50 THEN N'High' + WHEN [e].[Value] > 10 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt new file mode 100644 index 0000000..1ae6355 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'Very High' + WHEN [e].[Value] > 50 THEN N'High' + WHEN [e].[Value] > 10 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt new file mode 100644 index 0000000..1ae6355 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'Very High' + WHEN [e].[Value] > 50 THEN N'High' + WHEN [e].[Value] > 10 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file From 3c91bf9ff734f380a4396ec4ff5346a823eba93f Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sun, 15 Feb 2026 10:09:03 +0100 Subject: [PATCH 08/26] Update docs --- docs/BlockBodiedMethods.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md index 9dc6e7a..c74bed8 100644 --- a/docs/BlockBodiedMethods.md +++ b/docs/BlockBodiedMethods.md @@ -104,6 +104,32 @@ public string GetStatus() } ``` +### 7. Multiple Early Returns (converted to nested ternary expressions) +```csharp +[Projectable] +public string GetValueCategory() +{ + if (Value > 100) + { + return "Very High"; + } + + if (Value > 50) + { + return "High"; + } + + if (Value > 10) + { + return "Medium"; + } + + return "Low"; +} + +// Converted to: Value > 100 ? "Very High" : (Value > 50 ? "High" : (Value > 10 ? "Medium" : "Low")) +``` + ## Limitations and Warnings The source generator will produce **warning EFP0003** when it encounters unsupported statements in block-bodied methods: From e2b1fad5504c358eaed019d542231be3c9c248ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 09:32:52 +0000 Subject: [PATCH 09/26] Address code review feedback - fix semantics and scoping issues - Add diagnostic for empty block bodies (EFP0003) - Fix transitive local variable inlining (var a = 1; var b = a + 2; now fully expands) - Add warning when local variables are used multiple times (semantics preservation) - Prevent locals in nested blocks from leaking into outer scopes - Fix documentation to show compilable C# code (no implicit returns) - Add tests for transitive locals and nested block restrictions - All 197 tests passing (96 net8.0 + 101 net10.0) Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- docs/BlockBodiedMethods.md | 20 ++++- .../BlockStatementConverter.cs | 75 ++++++++++++++++++- ...estedBlock_ProducesDiagnostic.verified.txt | 3 + ..._WithTransitiveLocalVariables.verified.txt | 17 +++++ .../ProjectionExpressionGeneratorTests.cs | 62 +++++++++++++++ 5 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md index c74bed8..494b5fe 100644 --- a/docs/BlockBodiedMethods.md +++ b/docs/BlockBodiedMethods.md @@ -59,8 +59,23 @@ public int CalculateDouble() var doubled = Value * 2; return doubled + 5; } + +// Transitive inlining is also supported: +[Projectable] +public int CalculateComplex() +{ + var a = Value * 2; + var b = a + 5; + return b + 10; // Fully expanded to: Value * 2 + 5 + 10 +} ``` +**⚠️ Important Notes:** +- Local variables are inlined at each usage point, which duplicates the initializer expression +- If a local variable is used multiple times, the generator will emit a warning (EFP0003) as this could change semantics if the initializer has side effects +- Local variables can only be declared at the method body level, not inside nested blocks (if/switch/etc.) +- Variables are fully expanded transitively (variables that reference other variables are fully inlined) + ### 5. Switch Statements (converted to nested ternary expressions) ```csharp [Projectable] @@ -82,6 +97,7 @@ public string GetValueLabel() ### 6. If Statements Without Else (uses default value) ```csharp +// Pattern 1: Explicit null return [Projectable] public int? GetPremiumIfActive() { @@ -89,10 +105,10 @@ public int? GetPremiumIfActive() { return Value * 2; } - // Implicitly returns null (default for int?) + return null; // Explicit return for all code paths } -// Or with explicit fallback: +// Pattern 2: Explicit fallback return [Projectable] public string GetStatus() { diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index d5817e2..eead01d 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -28,6 +30,13 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax { if (block.Statements.Count == 0) { + var diagnostic = Diagnostic.Create( + Diagnostics.UnsupportedStatementInBlockBody, + block.GetLocation(), + memberName, + "Block body must contain at least one statement" + ); + _context.ReportDiagnostic(diagnostic); return null; } @@ -145,7 +154,13 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec var variableName = variable.Identifier.Text; // Rewrite the initializer expression NOW while it's still in the tree - _localVariables[variableName] = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value); + var rewrittenInitializer = (ExpressionSyntax)_expressionRewriter.Visit(variable.Initializer.Value); + + // Also expand any previously defined local variables in this initializer + // This ensures transitive inlining (e.g., var a = 1; var b = a + 2; return b; -> 1 + 2) + rewrittenInitializer = ReplaceLocalVariables(rewrittenInitializer); + + _localVariables[variableName] = rewrittenInitializer; } return true; @@ -165,6 +180,17 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec return TryConvertSwitchStatement(switchStmt, memberName); case BlockSyntax blockStmt: + // Prevent locals declared in nested blocks from leaking into outer scopes + var nestedLocal = blockStmt.DescendantNodes() + .OfType() + .FirstOrDefault(); + + if (nestedLocal is not null) + { + ReportUnsupportedStatement(nestedLocal, memberName, "Local declarations in nested blocks are not supported"); + return null; + } + return TryConvertStatements(blockStmt.Statements.ToList(), memberName); case ExpressionStatementSyntax: @@ -368,6 +394,28 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) { + // Count how many times each local variable is referenced + var referenceCounter = new LocalVariableReferenceCounter(_localVariables.Keys); + referenceCounter.Visit(expression); + + // Warn if any local variable is referenced more than once (semantics could change due to duplication) + foreach (var kvp in referenceCounter.ReferenceCounts) + { + if (kvp.Value > 1) + { + // This is a warning because inlining still produces correct results for pure expressions, + // but could change behavior if the initializer has side effects or is expensive + var diagnostic = Diagnostic.Create( + Diagnostics.UnsupportedStatementInBlockBody, + expression.GetLocation(), + "local variable", + $"Local variable '{kvp.Key}' is referenced {kvp.Value} times and will be inlined at each use. " + + "This may change semantics if the initializer has side effects or is evaluated multiple times." + ); + _context.ReportDiagnostic(diagnostic); + } + } + // Use a rewriter to replace local variable references with their initializer expressions var rewriter = new LocalVariableReplacer(_localVariables); return (ExpressionSyntax)rewriter.Visit(expression); @@ -384,6 +432,31 @@ private void ReportUnsupportedStatement(StatementSyntax statement, string member _context.ReportDiagnostic(diagnostic); } + private class LocalVariableReferenceCounter : CSharpSyntaxWalker + { + private readonly HashSet _localVariableNames; + public Dictionary ReferenceCounts { get; } = new Dictionary(); + + public LocalVariableReferenceCounter(IEnumerable localVariableNames) + { + _localVariableNames = new HashSet(localVariableNames); + foreach (var name in localVariableNames) + { + ReferenceCounts[name] = 0; + } + } + + public override void VisitIdentifierName(IdentifierNameSyntax node) + { + var identifier = node.Identifier.Text; + if (_localVariableNames.Contains(identifier)) + { + ReferenceCounts[identifier]++; + } + base.VisitIdentifierName(node); + } + } + private class LocalVariableReplacer : CSharpSyntaxRewriter { private readonly Dictionary _localVariables; diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt new file mode 100644 index 0000000..587f792 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic.verified.txt @@ -0,0 +1,3 @@ +[ + (13,17): warning EFP0003: Method 'Foo' contains an unsupported statement: Local declarations in nested blocks are not supported +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt new file mode 100644 index 0000000..24ae821 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar * 2 + 5 + 10; + } + } +} \ 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 3d220b1..c11232b 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2131,6 +2131,68 @@ public int Foo() return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task BlockBodiedMethod_WithTransitiveLocalVariables() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + var a = Bar * 2; + var b = a + 5; + return b + 10; + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + if (Bar > 10) + { + var temp = Bar * 2; + return temp; + } + return 0; + } + } +} +", expectedToCompile: true); + + var result = RunGenerator(compilation); + + // Should have a diagnostic about locals in nested blocks + Assert.NotEmpty(result.Diagnostics); + Assert.Contains(result.Diagnostics, d => d.Id == "EFP0003"); + + return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); + } + [Fact] public Task BlockBodiedMethod_WithMultipleParameters() { From f7f296b4efb35d1bcda48b3baac7daa2f7f639ff Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sun, 15 Feb 2026 19:14:01 +0100 Subject: [PATCH 10/26] Improve switch expression support and improve variable handling --- .../BlockStatementConverter.cs | 46 --- .../ExpressionSyntaxRewriter.cs | 43 +- ...urn_WorksCorrectly.DotNet10_0.verified.txt | 2 + ...turn_WorksCorrectly.DotNet9_0.verified.txt | 2 + ...hmeticInReturn_WorksCorrectly.verified.txt | 2 + ...urn_WorksCorrectly.DotNet10_0.verified.txt | 5 + ...turn_WorksCorrectly.DotNet9_0.verified.txt | 5 + ....BooleanReturn_WorksCorrectly.verified.txt | 5 + ...ess_WorksCorrectly.DotNet10_0.verified.txt | 2 + ...cess_WorksCorrectly.DotNet9_0.verified.txt | 2 + ...ditionalAccess_WorksCorrectly.verified.txt | 2 + ...ion_WorksCorrectly.DotNet10_0.verified.txt | 5 + ...tion_WorksCorrectly.DotNet9_0.verified.txt | 5 + ...alWithNegation_WorksCorrectly.verified.txt | 5 + ...se_WithEarlyReturn.DotNet10_0.verified.txt | 6 + ...use_WithEarlyReturn.DotNet9_0.verified.txt | 6 + ...s.GuardClause_WithEarlyReturn.verified.txt | 6 + ...linedMultipleTimes.DotNet10_0.verified.txt | 2 + ...nlinedMultipleTimes.DotNet9_0.verified.txt | 2 + ...eReuse_IsInlinedMultipleTimes.verified.txt | 2 + ...thMultiplePatterns.DotNet10_0.verified.txt | 9 + ...ithMultiplePatterns.DotNet9_0.verified.txt | 9 + ...ndSwitch_WithMultiplePatterns.verified.txt | 9 + ...reInlinedCorrectly.DotNet10_0.verified.txt | 2 + ...AreInlinedCorrectly.DotNet9_0.verified.txt | 2 + ...Variables_AreInlinedCorrectly.verified.txt | 2 + ...thLogicalOperators.DotNet10_0.verified.txt | 7 + ...ithLogicalOperators.DotNet9_0.verified.txt | 7 + ...itionals_WithLogicalOperators.verified.txt | 7 + ...nIf_WorksCorrectly.DotNet10_0.verified.txt | 9 + ...InIf_WorksCorrectly.DotNet9_0.verified.txt | 9 + ...stedSwitchInIf_WorksCorrectly.verified.txt | 9 + ...ary_WorksCorrectly.DotNet10_0.verified.txt | 6 + ...nary_WorksCorrectly.DotNet9_0.verified.txt | 6 + ....NestedTernary_WorksCorrectly.verified.txt | 6 + ...ing_WorksCorrectly.DotNet10_0.verified.txt | 2 + ...cing_WorksCorrectly.DotNet9_0.verified.txt | 2 + ...NullCoalescing_WorksCorrectly.verified.txt | 2 + ...ion_WorksCorrectly.DotNet10_0.verified.txt | 2 + ...tion_WorksCorrectly.DotNet9_0.verified.txt | 2 + ...gInterpolation_WorksCorrectly.verified.txt | 2 + ...hExpression_Simple.DotNet10_0.verified.txt | 7 + ...chExpression_Simple.DotNet9_0.verified.txt | 7 + ...Tests.SwitchExpression_Simple.verified.txt | 7 + ...ession_WithDiscard.DotNet10_0.verified.txt | 7 + ...ression_WithDiscard.DotNet9_0.verified.txt | 7 + ....SwitchExpression_WithDiscard.verified.txt | 7 + ...use_WorksCorrectly.DotNet10_0.verified.txt | 7 + ...ause_WorksCorrectly.DotNet9_0.verified.txt | 7 + ...WithWhenClause_WorksCorrectly.verified.txt | 7 + ...ion_WorksCorrectly.DotNet10_0.verified.txt | 5 + ...sion_WorksCorrectly.DotNet9_0.verified.txt | 5 + ...naryExpression_WorksCorrectly.verified.txt | 5 + .../BlockBodiedMethodTests.cs | 370 ++++++++++++++++++ 54 files changed, 666 insertions(+), 48 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index eead01d..e768940 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -394,28 +394,6 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) { - // Count how many times each local variable is referenced - var referenceCounter = new LocalVariableReferenceCounter(_localVariables.Keys); - referenceCounter.Visit(expression); - - // Warn if any local variable is referenced more than once (semantics could change due to duplication) - foreach (var kvp in referenceCounter.ReferenceCounts) - { - if (kvp.Value > 1) - { - // This is a warning because inlining still produces correct results for pure expressions, - // but could change behavior if the initializer has side effects or is expensive - var diagnostic = Diagnostic.Create( - Diagnostics.UnsupportedStatementInBlockBody, - expression.GetLocation(), - "local variable", - $"Local variable '{kvp.Key}' is referenced {kvp.Value} times and will be inlined at each use. " + - "This may change semantics if the initializer has side effects or is evaluated multiple times." - ); - _context.ReportDiagnostic(diagnostic); - } - } - // Use a rewriter to replace local variable references with their initializer expressions var rewriter = new LocalVariableReplacer(_localVariables); return (ExpressionSyntax)rewriter.Visit(expression); @@ -432,30 +410,6 @@ private void ReportUnsupportedStatement(StatementSyntax statement, string member _context.ReportDiagnostic(diagnostic); } - private class LocalVariableReferenceCounter : CSharpSyntaxWalker - { - private readonly HashSet _localVariableNames; - public Dictionary ReferenceCounts { get; } = new Dictionary(); - - public LocalVariableReferenceCounter(IEnumerable localVariableNames) - { - _localVariableNames = new HashSet(localVariableNames); - foreach (var name in localVariableNames) - { - ReferenceCounts[name] = 0; - } - } - - public override void VisitIdentifierName(IdentifierNameSyntax node) - { - var identifier = node.Identifier.Text; - if (_localVariableNames.Contains(identifier)) - { - ReferenceCounts[identifier]++; - } - base.VisitIdentifierName(node); - } - } private class LocalVariableReplacer : CSharpSyntaxRewriter { diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index f51c8f6..2b152dd 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; @@ -386,8 +386,47 @@ private ExpressionSyntax CreateMethodCallOnEnumValue(IMethodSymbol methodSymbol, continue; } + // Handle relational patterns (<=, <, >=, >) + if (arm.Pattern is RelationalPatternSyntax relational) + { + // Map the pattern operator token to a binary expression kind + var binaryKind = relational.OperatorToken.Kind() switch + { + 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) + ); + } + + currentExpression = SyntaxFactory.ConditionalExpression( + condition, + armExpression, + currentExpression + ); + + continue; + } + throw new InvalidOperationException( - $"Switch expressions rewriting supports only constant values and declaration patterns (Type var). " + + $"Switch expressions rewriting supports constant values, relational patterns (<=, <, >=, >), and declaration patterns (Type var). " + $"Unsupported pattern: {arm.Pattern.GetType().Name}" ); } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..3eaf767 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT (CAST([e].[Value] AS float) / 100.0E0) * 50.0E0 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..3eaf767 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT (CAST([e].[Value] AS float) / 100.0E0) * 50.0E0 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt new file mode 100644 index 0000000..3eaf767 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt @@ -0,0 +1,2 @@ +SELECT (CAST([e].[Value] AS float) / 100.0E0) * 50.0E0 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..e5b6efb --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..e5b6efb --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt new file mode 100644 index 0000000..e5b6efb --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..ba1f2c1 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT CAST(LEN([e].[Name]) AS int) +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..ba1f2c1 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT CAST(LEN([e].[Name]) AS int) +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt new file mode 100644 index 0000000..ba1f2c1 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt @@ -0,0 +1,2 @@ +SELECT CAST(LEN([e].[Name]) AS int) +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..4d0592a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(0 AS bit) THEN N'Not Active' + ELSE N'Active' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..4d0592a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(0 AS bit) THEN N'Not Active' + ELSE N'Active' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt new file mode 100644 index 0000000..4d0592a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(0 AS bit) THEN N'Not Active' + ELSE N'Active' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt new file mode 100644 index 0000000..a29be77 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(0 AS bit) THEN 0 + WHEN [e].[Value] < 0 THEN 0 + ELSE [e].[Value] * 2 +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt new file mode 100644 index 0000000..a29be77 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(0 AS bit) THEN 0 + WHEN [e].[Value] < 0 THEN 0 + ELSE [e].[Value] * 2 +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt new file mode 100644 index 0000000..a29be77 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(0 AS bit) THEN 0 + WHEN [e].[Value] < 0 THEN 0 + ELSE [e].[Value] * 2 +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt new file mode 100644 index 0000000..eec38d9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + [e].[Value] * 2 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt new file mode 100644 index 0000000..eec38d9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + [e].[Value] * 2 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt new file mode 100644 index 0000000..eec38d9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + [e].[Value] * 2 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt new file mode 100644 index 0000000..257f6f0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] > 100 THEN N'Active High' + WHEN [e].[Value] > 50 THEN N'Active Medium' + ELSE N'Active Low' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt new file mode 100644 index 0000000..257f6f0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] > 100 THEN N'Active High' + WHEN [e].[Value] > 50 THEN N'Active Medium' + ELSE N'Active Low' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt new file mode 100644 index 0000000..257f6f0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] > 100 THEN N'Active High' + WHEN [e].[Value] > 50 THEN N'Active Medium' + ELSE N'Active Low' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..4a903b0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + [e].[Value] * 3 + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..4a903b0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + [e].[Value] * 3 + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt new file mode 100644 index 0000000..4a903b0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + [e].[Value] * 3 + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt new file mode 100644 index 0000000..6973619 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100 THEN N'Active High' + WHEN [e].[IsActive] = CAST(1 AS bit) OR [e].[Value] > 50 THEN N'Active or Medium' + WHEN [e].[IsActive] = CAST(0 AS bit) AND [e].[Value] <= 10 THEN N'Inactive Low' + ELSE N'Other' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt new file mode 100644 index 0000000..6973619 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100 THEN N'Active High' + WHEN [e].[IsActive] = CAST(1 AS bit) OR [e].[Value] > 50 THEN N'Active or Medium' + WHEN [e].[IsActive] = CAST(0 AS bit) AND [e].[Value] <= 10 THEN N'Inactive Low' + ELSE N'Other' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt new file mode 100644 index 0000000..6973619 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100 THEN N'Active High' + WHEN [e].[IsActive] = CAST(1 AS bit) OR [e].[Value] > 50 THEN N'Active or Medium' + WHEN [e].[IsActive] = CAST(0 AS bit) AND [e].[Value] <= 10 THEN N'Inactive Low' + ELSE N'Other' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..5f5a209 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] = 1 THEN N'Active One' + WHEN [e].[Value] = 2 THEN N'Active Two' + ELSE N'Active Other' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..5f5a209 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] = 1 THEN N'Active One' + WHEN [e].[Value] = 2 THEN N'Active Two' + ELSE N'Active Other' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt new file mode 100644 index 0000000..5f5a209 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] = 1 THEN N'Active One' + WHEN [e].[Value] = 2 THEN N'Active Two' + ELSE N'Active Other' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..9d42002 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + WHEN [e].[Value] > 50 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..9d42002 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + WHEN [e].[Value] > 50 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt new file mode 100644 index 0000000..9d42002 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt @@ -0,0 +1,6 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'High' + WHEN [e].[Value] > 50 THEN N'Medium' + ELSE N'Low' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..52f2a3e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT COALESCE([e].[Name], N'Unknown') +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..52f2a3e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT COALESCE([e].[Name], N'Unknown') +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt new file mode 100644 index 0000000..52f2a3e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt @@ -0,0 +1,2 @@ +SELECT COALESCE([e].[Name], N'Unknown') +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..e6bf43e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..e6bf43e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt new file mode 100644 index 0000000..e6bf43e --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt new file mode 100644 index 0000000..9ed7fa8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 THEN N'One' + WHEN [e].[Value] = 2 THEN N'Two' + WHEN [e].[Value] = 3 THEN N'Three' + ELSE N'Many' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt new file mode 100644 index 0000000..9ed7fa8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 THEN N'One' + WHEN [e].[Value] = 2 THEN N'Two' + WHEN [e].[Value] = 3 THEN N'Three' + ELSE N'Many' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt new file mode 100644 index 0000000..9ed7fa8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 THEN N'One' + WHEN [e].[Value] = 2 THEN N'Two' + WHEN [e].[Value] = 3 THEN N'Three' + ELSE N'Many' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt new file mode 100644 index 0000000..727148f --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] <= 2 THEN N'Low' + WHEN [e].[Value] <= 5 THEN N'Medium' + WHEN [e].[Value] <= 8 THEN N'High' + ELSE N'Critical' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt new file mode 100644 index 0000000..727148f --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] <= 2 THEN N'Low' + WHEN [e].[Value] <= 5 THEN N'Medium' + WHEN [e].[Value] <= 8 THEN N'High' + ELSE N'Critical' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt new file mode 100644 index 0000000..727148f --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] <= 2 THEN N'Low' + WHEN [e].[Value] <= 5 THEN N'Medium' + WHEN [e].[Value] <= 8 THEN N'High' + ELSE N'Critical' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..f2343d3 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active One' + WHEN [e].[Value] = 1 THEN N'Inactive One' + WHEN [e].[Value] > 10 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active High' + ELSE N'Other' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..f2343d3 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active One' + WHEN [e].[Value] = 1 THEN N'Inactive One' + WHEN [e].[Value] > 10 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active High' + ELSE N'Other' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt new file mode 100644 index 0000000..f2343d3 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt @@ -0,0 +1,7 @@ +SELECT CASE + WHEN [e].[Value] = 1 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active One' + WHEN [e].[Value] = 1 THEN N'Inactive One' + WHEN [e].[Value] > 10 AND [e].[IsActive] = CAST(1 AS bit) THEN N'Active High' + ELSE N'Other' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt new file mode 100644 index 0000000..f3f5c43 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active' + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt new file mode 100644 index 0000000..f3f5c43 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active' + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt new file mode 100644 index 0000000..f3f5c43 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN N'Active' + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs index a961442..37a6898 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs @@ -149,6 +149,193 @@ public Task MultipleEarlyReturns_ConvertedToNestedTernaries() return Verifier.Verify(query.ToQueryString()); } + + [Fact] + public Task NullCoalescing_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetNameOrDefault()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task ConditionalAccess_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetNameLength()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SwitchExpression_Simple() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetValueLabelModern()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SwitchExpression_WithDiscard() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetPriorityModern()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task MultipleLocalVariables_AreInlinedCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.CalculateComplex()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task NestedConditionals_WithLogicalOperators() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetComplexCategory()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task GuardClause_WithEarlyReturn() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetGuardedValue()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task NestedSwitchInIf_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetCombinedLogic()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task TernaryExpression_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetValueUsingTernary()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task NestedTernary_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetNestedTernary()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task MixedIfAndSwitch_WithMultiplePatterns() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetComplexMix()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task SwitchWithWhenClause_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetValueWithCondition()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task LocalVariableReuse_IsInlinedMultipleTimes() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.CalculateWithReuse()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BooleanReturn_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.IsHighValue()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task ConditionalWithNegation_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetInactiveStatus()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task StringInterpolation_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetFormattedValue()); + + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task ArithmeticInReturn_WorksCorrectly() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.CalculatePercentage()); + + return Verifier.Verify(query.ToQueryString()); + } } public static class EntityExtensions @@ -298,5 +485,188 @@ public static string GetValueCategory(this BlockBodiedMethodTests.Entity entity) return "Low"; } + + [Projectable] + public static string GetNameOrDefault(this BlockBodiedMethodTests.Entity entity) + { + return entity.Name ?? "Unknown"; + } + + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + public static int? GetNameLength(this BlockBodiedMethodTests.Entity entity) + { + return entity.Name?.Length; + } + + [Projectable] + public static string GetValueLabelModern(this BlockBodiedMethodTests.Entity entity) + { + return entity.Value switch + { + 1 => "One", + 2 => "Two", + 3 => "Three", + _ => "Many" + }; + } + + [Projectable] + public static string GetPriorityModern(this BlockBodiedMethodTests.Entity entity) + { + return entity.Value switch + { + <= 2 => "Low", + <= 5 => "Medium", + <= 8 => "High", + _ => "Critical" + }; + } + + [Projectable] + public static int CalculateComplex(this BlockBodiedMethodTests.Entity entity) + { + var doubled = entity.Value * 2; + var tripled = entity.Value * 3; + var sum = doubled + tripled; + return sum + 10; + } + + [Projectable] + public static string GetComplexCategory(this BlockBodiedMethodTests.Entity entity) + { + if (entity.IsActive && entity.Value > 100) + { + return "Active High"; + } + + if (entity.IsActive || entity.Value > 50) + { + return "Active or Medium"; + } + + if (!entity.IsActive && entity.Value <= 10) + { + return "Inactive Low"; + } + + return "Other"; + } + + [Projectable] + public static int GetGuardedValue(this BlockBodiedMethodTests.Entity entity) + { + if (!entity.IsActive) + { + return 0; + } + + if (entity.Value < 0) + { + return 0; + } + + return entity.Value * 2; + } + + [Projectable] + public static string GetCombinedLogic(this BlockBodiedMethodTests.Entity entity) + { + if (entity.IsActive) + { + switch (entity.Value) + { + case 1: + return "Active One"; + case 2: + return "Active Two"; + default: + return "Active Other"; + } + } + + return "Inactive"; + } + + [Projectable] + public static string GetValueUsingTernary(this BlockBodiedMethodTests.Entity entity) + { + return entity.IsActive ? "Active" : "Inactive"; + } + + [Projectable] + public static string GetNestedTernary(this BlockBodiedMethodTests.Entity entity) + { + return entity.Value > 100 ? "High" : entity.Value > 50 ? "Medium" : "Low"; + } + + [Projectable] + public static string GetComplexMix(this BlockBodiedMethodTests.Entity entity) + { + if (entity.IsActive) + { + return entity.Value switch + { + > 100 => "Active High", + > 50 => "Active Medium", + _ => "Active Low" + }; + } + + return "Inactive"; + } + + [Projectable] + public static string GetValueWithCondition(this BlockBodiedMethodTests.Entity entity) + { + return entity.Value switch + { + 1 when entity.IsActive => "Active One", + 1 => "Inactive One", + > 10 when entity.IsActive => "Active High", + _ => "Other" + }; + } + + [Projectable] + public static int CalculateWithReuse(this BlockBodiedMethodTests.Entity entity) + { + var doubled = entity.Value * 2; + return doubled + doubled; + } + + [Projectable] + public static bool IsHighValue(this BlockBodiedMethodTests.Entity entity) + { + if (entity.Value > 100) + { + return true; + } + return false; + } + + [Projectable] + public static string GetInactiveStatus(this BlockBodiedMethodTests.Entity entity) + { + if (!entity.IsActive) + { + return "Not Active"; + } + else + { + return "Active"; + } + } + + [Projectable] + public static string GetFormattedValue(this BlockBodiedMethodTests.Entity entity) + { + return $"Value: {entity.Value}"; + } + + [Projectable] + public static double CalculatePercentage(this BlockBodiedMethodTests.Entity entity) + { + return (double)entity.Value / 100.0 * 50.0; + } } } From 3b56faaca7a0f81e22ccde7b3656f47eaa2cfcf1 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sun, 15 Feb 2026 22:20:17 +0100 Subject: [PATCH 11/26] Remove redundant code and add test for projectables in block bodied methods --- .../BlockStatementConverter.cs | 64 ++-- ...urn_WorksCorrectly.DotNet10_0.verified.txt | 0 ...turn_WorksCorrectly.DotNet9_0.verified.txt | 0 ...hmeticInReturn_WorksCorrectly.verified.txt | 0 ...ers_WorksCorrectly.DotNet10_0.verified.txt | 0 ...ters_WorksCorrectly.DotNet9_0.verified.txt | 0 ...WithParameters_WorksCorrectly.verified.txt | 0 ...urn_WorksCorrectly.DotNet10_0.verified.txt | 0 ...turn_WorksCorrectly.DotNet9_0.verified.txt | 0 ....BooleanReturn_WorksCorrectly.verified.txt | 0 ...ranslatedCorrectly.DotNet10_0.verified.txt | 0 ...TranslatedCorrectly.DotNet9_0.verified.txt | 0 ...itional_IsTranslatedCorrectly.verified.txt | 0 ...ess_WorksCorrectly.DotNet10_0.verified.txt | 0 ...cess_WorksCorrectly.DotNet9_0.verified.txt | 0 ...ditionalAccess_WorksCorrectly.verified.txt | 0 ...ion_WorksCorrectly.DotNet10_0.verified.txt | 0 ...tion_WorksCorrectly.DotNet9_0.verified.txt | 0 ...alWithNegation_WorksCorrectly.verified.txt | 0 ...se_WithEarlyReturn.DotNet10_0.verified.txt | 0 ...use_WithEarlyReturn.DotNet9_0.verified.txt | 0 ...s.GuardClause_WithEarlyReturn.verified.txt | 0 ...ranslatedToTernary.DotNet10_0.verified.txt | 0 ...TranslatedToTernary.DotNet9_0.verified.txt | 0 ...atement_IsTranslatedToTernary.verified.txt | 0 ...utElse_UsesDefault.DotNet10_0.verified.txt | 0 ...outElse_UsesDefault.DotNet9_0.verified.txt | 0 ...sts.IfWithoutElse_UsesDefault.verified.txt | 0 ...WithFallbackReturn.DotNet10_0.verified.txt | 0 ..._WithFallbackReturn.DotNet9_0.verified.txt | 0 ...ithoutElse_WithFallbackReturn.verified.txt | 0 ...linedMultipleTimes.DotNet10_0.verified.txt | 0 ...nlinedMultipleTimes.DotNet9_0.verified.txt | 0 ...eReuse_IsInlinedMultipleTimes.verified.txt | 0 ...Variable_IsInlined.DotNet10_0.verified.txt | 0 ...lVariable_IsInlined.DotNet9_0.verified.txt | 0 ...Tests.LocalVariable_IsInlined.verified.txt | 0 ...thMultiplePatterns.DotNet10_0.verified.txt | 0 ...ithMultiplePatterns.DotNet9_0.verified.txt | 0 ...ndSwitch_WithMultiplePatterns.verified.txt | 0 ...dToNestedTernaries.DotNet10_0.verified.txt | 0 ...edToNestedTernaries.DotNet9_0.verified.txt | 0 ...ns_ConvertedToNestedTernaries.verified.txt | 0 ...reInlinedCorrectly.DotNet10_0.verified.txt | 0 ...AreInlinedCorrectly.DotNet9_0.verified.txt | 0 ...Variables_AreInlinedCorrectly.verified.txt | 0 ...thLogicalOperators.DotNet10_0.verified.txt | 0 ...ithLogicalOperators.DotNet9_0.verified.txt | 0 ...itionals_WithLogicalOperators.verified.txt | 0 ...tedToNestedTernary.DotNet10_0.verified.txt | 0 ...atedToNestedTernary.DotNet9_0.verified.txt | 0 ...e_IsTranslatedToNestedTernary.verified.txt | 0 ...nIf_WorksCorrectly.DotNet10_0.verified.txt | 0 ...InIf_WorksCorrectly.DotNet9_0.verified.txt | 0 ...stedSwitchInIf_WorksCorrectly.verified.txt | 0 ...ary_WorksCorrectly.DotNet10_0.verified.txt | 0 ...nary_WorksCorrectly.DotNet9_0.verified.txt | 0 ....NestedTernary_WorksCorrectly.verified.txt | 0 ...ing_WorksCorrectly.DotNet10_0.verified.txt | 0 ...cing_WorksCorrectly.DotNet9_0.verified.txt | 0 ...NullCoalescing_WorksCorrectly.verified.txt | 0 ..._IsTranslatedToSql.DotNet10_0.verified.txt | 0 ...s_IsTranslatedToSql.DotNet9_0.verified.txt | 0 ...pertyAccess_IsTranslatedToSql.verified.txt | 0 ..._IsTranslatedToSql.DotNet10_0.verified.txt | 0 ...n_IsTranslatedToSql.DotNet9_0.verified.txt | 0 ...impleReturn_IsTranslatedToSql.verified.txt | 0 ...ion_WorksCorrectly.DotNet10_0.verified.txt | 0 ...tion_WorksCorrectly.DotNet9_0.verified.txt | 0 ...gInterpolation_WorksCorrectly.verified.txt | 0 ...hExpression_Simple.DotNet10_0.verified.txt | 0 ...chExpression_Simple.DotNet9_0.verified.txt | 0 ...Tests.SwitchExpression_Simple.verified.txt | 0 ...ession_WithDiscard.DotNet10_0.verified.txt | 0 ...ression_WithDiscard.DotNet9_0.verified.txt | 0 ....SwitchExpression_WithDiscard.verified.txt | 0 ...chStatement_Simple.DotNet10_0.verified.txt | 0 ...tchStatement_Simple.DotNet9_0.verified.txt | 0 ...dTests.SwitchStatement_Simple.verified.txt | 0 ..._WithMultipleCases.DotNet10_0.verified.txt | 0 ...t_WithMultipleCases.DotNet9_0.verified.txt | 0 ...chStatement_WithMultipleCases.verified.txt | 0 ...use_WorksCorrectly.DotNet10_0.verified.txt | 0 ...ause_WorksCorrectly.DotNet9_0.verified.txt | 0 ...WithWhenClause_WorksCorrectly.verified.txt | 0 ...ion_WorksCorrectly.DotNet10_0.verified.txt | 0 ...sion_WorksCorrectly.DotNet9_0.verified.txt | 0 ...naryExpression_WorksCorrectly.verified.txt | 0 .../BlockBodiedMethodTests.cs | 2 +- .../BlockBodyProjectableCallTest.cs | 273 ++++++++++++++++++ ...Method_InCondition.DotNet10_0.verified.txt | 5 + ...eMethod_InCondition.DotNet9_0.verified.txt | 5 + ...ProjectableMethod_InCondition.verified.txt | 5 + ...thod_InEarlyReturn.DotNet10_0.verified.txt | 9 + ...ethod_InEarlyReturn.DotNet9_0.verified.txt | 9 + ...ojectableMethod_InEarlyReturn.verified.txt | 9 + ...nLogicalExpression.DotNet10_0.verified.txt | 5 + ...InLogicalExpression.DotNet9_0.verified.txt | 5 + ...bleMethod_InLogicalExpression.verified.txt | 5 + ...bleMethod_InReturn.DotNet10_0.verified.txt | 2 + ...ableMethod_InReturn.DotNet9_0.verified.txt | 2 + ...ingProjectableMethod_InReturn.verified.txt | 2 + ...bleMethod_InSwitch.DotNet10_0.verified.txt | 12 + ...ableMethod_InSwitch.DotNet9_0.verified.txt | 12 + ...ingProjectableMethod_InSwitch.verified.txt | 12 + ...InSwitchExpression.DotNet10_0.verified.txt | 19 ++ ..._InSwitchExpression.DotNet9_0.verified.txt | 19 ++ ...ableMethod_InSwitchExpression.verified.txt | 19 ++ ...leMethod_InTernary.DotNet10_0.verified.txt | 8 + ...bleMethod_InTernary.DotNet9_0.verified.txt | 8 + ...ngProjectableMethod_InTernary.verified.txt | 8 + ...bleMethod_Multiple.DotNet10_0.verified.txt | 2 + ...ableMethod_Multiple.DotNet9_0.verified.txt | 2 + ...ingProjectableMethod_Multiple.verified.txt | 2 + ...tableMethod_Nested.DotNet10_0.verified.txt | 2 + ...ctableMethod_Nested.DotNet9_0.verified.txt | 2 + ...llingProjectableMethod_Nested.verified.txt | 2 + ...tableMethod_Simple.DotNet10_0.verified.txt | 2 + ...ctableMethod_Simple.DotNet9_0.verified.txt | 2 + ...llingProjectableMethod_Simple.verified.txt | 2 + ..._WithLocalVariable.DotNet10_0.verified.txt | 2 + ...d_WithLocalVariable.DotNet9_0.verified.txt | 2 + ...tableMethod_WithLocalVariable.verified.txt | 2 + 123 files changed, 499 insertions(+), 44 deletions(-) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt (100%) rename tests/EntityFrameworkCore.Projectables.FunctionalTests/{ => BlockBodiedMethods}/BlockBodiedMethodTests.cs (99%) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index e768940..843930f 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -41,8 +41,7 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax } // Try to convert the block statements into an expression - var result = TryConvertStatements(block.Statements.ToList(), memberName); - return result; + return TryConvertStatements(block.Statements.ToList(), memberName); } private ExpressionSyntax? TryConvertStatements(List statements, string memberName) @@ -95,30 +94,6 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax return elseBody; } - // Check if we have a single if without else followed by a return (legacy path) - // This is now redundant with the above logic but kept for clarity and potential optimization - if (nonReturnStatements.Count == 1 && - nonReturnStatements[0] is IfStatementSyntax { Else: null } ifWithoutElse && - lastStatement is ReturnStatementSyntax singleFinalReturn) - { - // Convert: if (condition) { return x; } return y; - // To: condition ? x : y - var ifBody = TryConvertStatement(ifWithoutElse.Statement, memberName); - if (ifBody == null) - { - return null; - } - - var elseBody = TryConvertReturnStatement(singleFinalReturn, memberName); - if (elseBody == null) - { - return null; - } - - var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifWithoutElse.Condition); - return SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody); - } - // If we reach here, the pattern was not detected // Process local variable declarations before the final return foreach (var stmt in nonReturnStatements) @@ -226,7 +201,7 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec return expression; } - private ExpressionSyntax? TryConvertIfStatement(IfStatementSyntax ifStmt, string memberName) + private ConditionalExpressionSyntax? TryConvertIfStatement(IfStatementSyntax ifStmt, string memberName) { // Convert if-else to conditional (ternary) expression // First, rewrite the condition using the expression rewriter @@ -371,25 +346,28 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec { statements = statements.Take(statements.Count - 1).ToList(); } - - if (statements.Count == 0) + + if (statements.Count != 0) + { + return TryConvertStatements(statements, memberName); + } + + // Use the section's first label location for error reporting + var firstLabel = section.Labels.FirstOrDefault(); + if (firstLabel == null) { - // Use the section's first label location for error reporting - var firstLabel = section.Labels.FirstOrDefault(); - if (firstLabel != null) - { - var diagnostic = Diagnostic.Create( - Diagnostics.UnsupportedStatementInBlockBody, - firstLabel.GetLocation(), - memberName, - "Switch section must have at least one statement" - ); - _context.ReportDiagnostic(diagnostic); - } return null; } - - return TryConvertStatements(statements, memberName); + + var diagnostic = Diagnostic.Create( + Diagnostics.UnsupportedStatementInBlockBody, + firstLabel.GetLocation(), + memberName, + "Switch section must have at least one statement" + ); + _context.ReportDiagnostic(diagnostic); + return null; + } private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ArithmeticInReturn_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BlockMethodWithParameters_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.BooleanReturn_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ComplexConditional_IsTranslatedCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalAccess_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ConditionalWithNegation_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.GuardClause_WithEarlyReturn.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfElseStatement_IsTranslatedToTernary.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_UsesDefault.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.IfWithoutElse_WithFallbackReturn.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariableReuse_IsInlinedMultipleTimes.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.LocalVariable_IsInlined.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MixedIfAndSwitch_WithMultiplePatterns.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleEarlyReturns_ConvertedToNestedTernaries.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.MultipleLocalVariables_AreInlinedCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedConditionals_WithLogicalOperators.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedIfElse_IsTranslatedToNestedTernary.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedSwitchInIf_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NestedTernary_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.NullCoalescing_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.ReturnWithPropertyAccess_IsTranslatedToSql.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SimpleReturn_IsTranslatedToSql.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.StringInterpolation_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_Simple.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchExpression_WithDiscard.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_Simple.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchStatement_WithMultipleCases.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.SwitchWithWhenClause_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet10_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.DotNet9_0.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt similarity index 100% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.TernaryExpression_WorksCorrectly.verified.txt diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs similarity index 99% rename from tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs rename to tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs index 37a6898..9622f34 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs @@ -5,7 +5,7 @@ using VerifyXunit; using Xunit; -namespace EntityFrameworkCore.Projectables.FunctionalTests +namespace EntityFrameworkCore.Projectables.FunctionalTests.BlockBodiedMethods { [UsesVerify] public class BlockBodiedMethodTests diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs new file mode 100644 index 0000000..4d3fa96 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs @@ -0,0 +1,273 @@ +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System.Threading.Tasks; +using VerifyXunit; +using Xunit; + +namespace EntityFrameworkCore.Projectables.FunctionalTests.BlockBodiedMethods +{ + /// + /// Tests for calling projectable methods from within block-bodied methods + /// + [UsesVerify] + public class BlockBodyProjectableCallTests + { + public record Entity + { + public int Id { get; set; } + public int Value { get; set; } + public bool IsActive { get; set; } + public string? Name { get; set; } + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_Simple() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.GetAdjustedWithConstant()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_InReturn() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.GetDoubledValue()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_InCondition() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.GetCategoryBasedOnAdjusted()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_Multiple() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.CombineProjectableMethods()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_InSwitch() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.GetLabelBasedOnCategory()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_InSwitchExpression() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.GetDescriptionByLevel()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_WithLocalVariable() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.CalculateUsingProjectable()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_Nested() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.GetNestedProjectableCall()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_InEarlyReturn() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.GetStatusWithProjectableCheck()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_InTernary() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.GetConditionalProjectable()); + return Verifier.Verify(query.ToQueryString()); + } + + [Fact] + public Task BlockBodyCallingProjectableMethod_InLogicalExpression() + { + using var dbContext = new SampleDbContext(); + var query = dbContext.Set() + .Select(x => x.IsComplexCondition()); + return Verifier.Verify(query.ToQueryString()); + } + } + + public static class ProjectableCallExtensions + { + // Base projectable methods (helper methods) + [Projectable] + public static int GetConstant(this BlockBodyProjectableCallTests.Entity entity) + { + return 42; + } + + [Projectable] + public static int GetDoubled(this BlockBodyProjectableCallTests.Entity entity) + { + return entity.Value * 2; + } + + [Projectable] + public static string GetCategory(this BlockBodyProjectableCallTests.Entity entity) + { + if (entity.Value > 100) + return "High"; + else + return "Low"; + } + + [Projectable] + public static string GetLevel(this BlockBodyProjectableCallTests.Entity entity) + { + if (entity.Value > 100) return "Level3"; + if (entity.Value > 50) return "Level2"; + return "Level1"; + } + + [Projectable] + public static bool IsHighValue(this BlockBodyProjectableCallTests.Entity entity) + { + return entity.Value > 100; + } + + // Block-bodied methods calling projectable methods + + [Projectable] + public static int GetAdjustedWithConstant(this BlockBodyProjectableCallTests.Entity entity) + { + return entity.Value + entity.GetConstant(); + } + + [Projectable] + public static int GetDoubledValue(this BlockBodyProjectableCallTests.Entity entity) + { + var doubled = entity.GetDoubled(); + return doubled; + } + + [Projectable] + public static string GetCategoryBasedOnAdjusted(this BlockBodyProjectableCallTests.Entity entity) + { + if (entity.GetDoubled() > 200) + { + return "Very High"; + } + else + { + return "Normal"; + } + } + + [Projectable] + public static int CombineProjectableMethods(this BlockBodyProjectableCallTests.Entity entity) + { + return entity.GetDoubled() + entity.GetConstant(); + } + + [Projectable] + public static string GetLabelBasedOnCategory(this BlockBodyProjectableCallTests.Entity entity) + { + switch (entity.GetCategory()) + { + case "High": + return "Premium"; + case "Low": + return "Standard"; + default: + return "Unknown"; + } + } + + [Projectable] + public static string GetDescriptionByLevel(this BlockBodyProjectableCallTests.Entity entity) + { + return entity.GetLevel() switch + { + "Level3" => "Expert", + "Level2" => "Intermediate", + "Level1" => "Beginner", + _ => "Unknown" + }; + } + + [Projectable] + public static int CalculateUsingProjectable(this BlockBodyProjectableCallTests.Entity entity) + { + var doubled = entity.GetDoubled(); + var withConstant = doubled + entity.GetConstant(); + return withConstant * 2; + } + + [Projectable] + public static int GetNestedProjectableCall(this BlockBodyProjectableCallTests.Entity entity) + { + return entity.GetAdjustedWithConstant() + 10; + } + + [Projectable] + public static string GetStatusWithProjectableCheck(this BlockBodyProjectableCallTests.Entity entity) + { + if (entity.IsHighValue()) + return "Premium"; + + if (entity.GetCategory() == "High") + return "Standard High"; + + return "Normal"; + } + + [Projectable] + public static string GetConditionalProjectable(this BlockBodyProjectableCallTests.Entity entity) + { + return entity.IsActive ? entity.GetCategory() : "Inactive"; + } + + // [Projectable] + // public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity entity) + // { + // var doubled = entity.GetDoubled(); + // + // if (doubled > 200) + // { + // return entity.GetCategory() + " Priority"; + // } + // + // return entity.GetLevel(); + // } + + [Projectable] + public static bool IsComplexCondition(this BlockBodyProjectableCallTests.Entity entity) + { + return entity.IsActive && entity.IsHighValue() || entity.GetDoubled() > 150; + } + } +} diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet10_0.verified.txt new file mode 100644 index 0000000..478d0ba --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet10_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] * 2 > 200 THEN N'Very High' + ELSE N'Normal' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet9_0.verified.txt new file mode 100644 index 0000000..478d0ba --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.DotNet9_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] * 2 > 200 THEN N'Very High' + ELSE N'Normal' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.verified.txt new file mode 100644 index 0000000..478d0ba --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InCondition.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN [e].[Value] * 2 > 200 THEN N'Very High' + ELSE N'Normal' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet10_0.verified.txt new file mode 100644 index 0000000..bd650a0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet10_0.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'Premium' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'High' THEN N'Standard High' + ELSE N'Normal' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet9_0.verified.txt new file mode 100644 index 0000000..bd650a0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.DotNet9_0.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'Premium' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'High' THEN N'Standard High' + ELSE N'Normal' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.verified.txt new file mode 100644 index 0000000..bd650a0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InEarlyReturn.verified.txt @@ -0,0 +1,9 @@ +SELECT CASE + WHEN [e].[Value] > 100 THEN N'Premium' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'High' THEN N'Standard High' + ELSE N'Normal' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet10_0.verified.txt new file mode 100644 index 0000000..de3373a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet10_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN ([e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100) OR [e].[Value] * 2 > 150 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet9_0.verified.txt new file mode 100644 index 0000000..de3373a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.DotNet9_0.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN ([e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100) OR [e].[Value] * 2 > 150 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.verified.txt new file mode 100644 index 0000000..de3373a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InLogicalExpression.verified.txt @@ -0,0 +1,5 @@ +SELECT CASE + WHEN ([e].[IsActive] = CAST(1 AS bit) AND [e].[Value] > 100) OR [e].[Value] * 2 > 150 THEN CAST(1 AS bit) + ELSE CAST(0 AS bit) +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet10_0.verified.txt new file mode 100644 index 0000000..dea1914 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet9_0.verified.txt new file mode 100644 index 0000000..dea1914 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.verified.txt new file mode 100644 index 0000000..dea1914 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InReturn.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet10_0.verified.txt new file mode 100644 index 0000000..927c6ff --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet10_0.verified.txt @@ -0,0 +1,12 @@ +SELECT CASE + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'High' THEN N'Premium' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'Low' THEN N'Standard' + ELSE N'Unknown' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet9_0.verified.txt new file mode 100644 index 0000000..927c6ff --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.DotNet9_0.verified.txt @@ -0,0 +1,12 @@ +SELECT CASE + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'High' THEN N'Premium' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'Low' THEN N'Standard' + ELSE N'Unknown' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.verified.txt new file mode 100644 index 0000000..927c6ff --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitch.verified.txt @@ -0,0 +1,12 @@ +SELECT CASE + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'High' THEN N'Premium' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END = N'Low' THEN N'Standard' + ELSE N'Unknown' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet10_0.verified.txt new file mode 100644 index 0000000..409a445 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet10_0.verified.txt @@ -0,0 +1,19 @@ +SELECT CASE + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level3' THEN N'Expert' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level2' THEN N'Intermediate' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level1' THEN N'Beginner' + ELSE N'Unknown' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet9_0.verified.txt new file mode 100644 index 0000000..409a445 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.DotNet9_0.verified.txt @@ -0,0 +1,19 @@ +SELECT CASE + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level3' THEN N'Expert' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level2' THEN N'Intermediate' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level1' THEN N'Beginner' + ELSE N'Unknown' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.verified.txt new file mode 100644 index 0000000..409a445 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InSwitchExpression.verified.txt @@ -0,0 +1,19 @@ +SELECT CASE + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level3' THEN N'Expert' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level2' THEN N'Intermediate' + WHEN CASE + WHEN [e].[Value] > 100 THEN N'Level3' + WHEN [e].[Value] > 50 THEN N'Level2' + ELSE N'Level1' + END = N'Level1' THEN N'Beginner' + ELSE N'Unknown' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet10_0.verified.txt new file mode 100644 index 0000000..ad971d0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet10_0.verified.txt @@ -0,0 +1,8 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet9_0.verified.txt new file mode 100644 index 0000000..ad971d0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.DotNet9_0.verified.txt @@ -0,0 +1,8 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.verified.txt new file mode 100644 index 0000000..ad971d0 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_InTernary.verified.txt @@ -0,0 +1,8 @@ +SELECT CASE + WHEN [e].[IsActive] = CAST(1 AS bit) THEN CASE + WHEN [e].[Value] > 100 THEN N'High' + ELSE N'Low' + END + ELSE N'Inactive' +END +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet10_0.verified.txt new file mode 100644 index 0000000..69eb4b8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet9_0.verified.txt new file mode 100644 index 0000000..69eb4b8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.verified.txt new file mode 100644 index 0000000..69eb4b8 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Multiple.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet10_0.verified.txt new file mode 100644 index 0000000..72fc7ea --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 42 + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet9_0.verified.txt new file mode 100644 index 0000000..72fc7ea --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 42 + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.verified.txt new file mode 100644 index 0000000..72fc7ea --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Nested.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 42 + 10 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet10_0.verified.txt new file mode 100644 index 0000000..0bb6121 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet9_0.verified.txt new file mode 100644 index 0000000..0bb6121 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.verified.txt new file mode 100644 index 0000000..0bb6121 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_Simple.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] + 42 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt new file mode 100644 index 0000000..0294ea7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 84 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt new file mode 100644 index 0000000..0294ea7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 84 +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt new file mode 100644 index 0000000..0294ea7 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Value] * 2 + 84 +FROM [Entity] AS [e] \ No newline at end of file From ff4feb1670e7fbff8b4a9af25b7ca9eea527dfb6 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Sun, 15 Feb 2026 22:27:12 +0100 Subject: [PATCH 12/26] Fix new case --- .../BlockStatementConverter.cs | 56 ++++++++++++------- .../BlockBodyProjectableCallTest.cs | 24 ++++---- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index 843930f..2093d98 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -61,14 +61,39 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax var nonReturnStatements = statements.Take(statements.Count - 1).ToList(); var lastStatement = statements.Last(); + // First, process any local variable declarations at the beginning + var localDeclStatements = new List(); + var remainingStatements = new List(); + + foreach (var stmt in nonReturnStatements) + { + if (stmt is LocalDeclarationStatementSyntax localDecl) + { + localDeclStatements.Add(localDecl); + } + else + { + remainingStatements.Add(stmt); + } + } + + // Process local variable declarations first + foreach (var localDecl in localDeclStatements) + { + if (!TryProcessLocalDeclaration(localDecl, memberName)) + { + return null; + } + } + // Check if we have a pattern like multiple if statements without else followed by a final return: - // if (a) return 1; if (b) return 2; return 3; + // var x = ...; if (a) return 1; if (b) return 2; return 3; // This can be converted to nested ternaries: a ? 1 : (b ? 2 : 3) if (lastStatement is ReturnStatementSyntax finalReturn && - nonReturnStatements.All(s => s is IfStatementSyntax { Else: null })) + remainingStatements.All(s => s is IfStatementSyntax { Else: null })) { - // All non-return statements are if statements without else - var ifStatements = nonReturnStatements.Cast().ToList(); + // All remaining non-return statements are if statements without else + var ifStatements = remainingStatements.Cast().ToList(); // Start with the final return as the base expression var elseBody = TryConvertReturnStatement(finalReturn, memberName); @@ -87,29 +112,22 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax return null; } + // Rewrite the condition and replace any local variables var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifStmt.Condition); + condition = ReplaceLocalVariables(condition); + elseBody = SyntaxFactory.ConditionalExpression(condition, ifBody, elseBody); } return elseBody; } - // If we reach here, the pattern was not detected - // Process local variable declarations before the final return - foreach (var stmt in nonReturnStatements) + // If there are any remaining non-if statements, they're not supported + if (remainingStatements.Count > 0) { - if (stmt is LocalDeclarationStatementSyntax localDecl) - { - if (!TryProcessLocalDeclaration(localDecl, memberName)) - { - return null; - } - } - else - { - ReportUnsupportedStatement(stmt, memberName, "Only local variable declarations are supported before the return statement"); - return null; - } + ReportUnsupportedStatement(remainingStatements[0], memberName, + "Only local variable declarations and if statements without else (with return) are supported before the final return statement"); + return null; } // Convert the final statement (should be a return) diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs index 4d3fa96..c09237b 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs @@ -251,18 +251,18 @@ public static string GetConditionalProjectable(this BlockBodyProjectableCallTest return entity.IsActive ? entity.GetCategory() : "Inactive"; } - // [Projectable] - // public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity entity) - // { - // var doubled = entity.GetDoubled(); - // - // if (doubled > 200) - // { - // return entity.GetCategory() + " Priority"; - // } - // - // return entity.GetLevel(); - // } + [Projectable] + public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity entity) + { + var doubled = entity.GetDoubled(); + + if (doubled > 200) + { + return entity.GetCategory() + " Priority"; + } + + return entity.GetLevel(); + } [Projectable] public static bool IsComplexCondition(this BlockBodyProjectableCallTests.Entity entity) From 7ba8a84d5ab03529d0f550711fccd6a2bb3a8f60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:35:16 +0000 Subject: [PATCH 13/26] Fix local variable replacement in conditions and switch expressions - Apply ReplaceLocalVariables to if statement conditions - Apply ReplaceLocalVariables to switch expressions - Apply ReplaceLocalVariables to case label values - Remove double BOM character from ExpressionSyntaxRewriter.cs - Fix documentation to match actual behavior (no multiple usage warning) - Add tests for locals in if conditions and switch expressions - All 201 tests passing (98 net8.0 + 103 net10.0) Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- docs/BlockBodiedMethods.md | 2 +- .../BlockStatementConverter.cs | 12 +++- .../ExpressionSyntaxRewriter.cs | 2 +- ...diedMethod_LocalInIfCondition.verified.txt | 17 +++++ ...ethod_LocalInSwitchExpression.verified.txt | 17 +++++ .../ProjectionExpressionGeneratorTests.cs | 71 +++++++++++++++++++ 6 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md index 494b5fe..fe19c69 100644 --- a/docs/BlockBodiedMethods.md +++ b/docs/BlockBodiedMethods.md @@ -72,7 +72,7 @@ public int CalculateComplex() **⚠️ Important Notes:** - Local variables are inlined at each usage point, which duplicates the initializer expression -- If a local variable is used multiple times, the generator will emit a warning (EFP0003) as this could change semantics if the initializer has side effects +- If a local variable is used multiple times, its initializer expression is duplicated at each usage, which can change semantics if the initializer has side effects - Local variables can only be declared at the method body level, not inside nested blocks (if/switch/etc.) - Variables are fully expanded transitively (variables that reference other variables are fully inlined) diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index 2093d98..5192612 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -224,6 +224,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec // Convert if-else to conditional (ternary) expression // First, rewrite the condition using the expression rewriter var condition = (ExpressionSyntax)_expressionRewriter.Visit(ifStmt.Condition); + + // Then replace any local variable references with their already-rewritten initializers + condition = ReplaceLocalVariables(condition); var whenTrue = TryConvertStatement(ifStmt.Statement, memberName); if (whenTrue == null) @@ -264,6 +267,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec // Process sections in reverse order to build from the default case up var switchExpression = (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression); + // Replace any local variable references in the switch expression + switchExpression = ReplaceLocalVariables(switchExpression); + ExpressionSyntax? currentExpression; // Find default case first @@ -317,10 +323,14 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec { if (label is CaseSwitchLabelSyntax caseLabel) { + // Rewrite and replace locals in case label value + var caseLabelValue = (ExpressionSyntax)_expressionRewriter.Visit(caseLabel.Value); + caseLabelValue = ReplaceLocalVariables(caseLabelValue); + var labelCondition = SyntaxFactory.BinaryExpression( SyntaxKind.EqualsExpression, switchExpression, - (ExpressionSyntax)_expressionRewriter.Visit(caseLabel.Value) + caseLabelValue ); condition = condition == null diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index 2b152dd..ef46396 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt new file mode 100644 index 0000000..e940c26 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar * 2 > 10 ? 1 : 0; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt new file mode 100644 index 0000000..0a7e7da --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar * 2 == 2 ? "Two" : @this.Bar * 2 == 4 ? "Four" : "Other"; + } + } +} \ 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 c11232b..3254530 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2160,6 +2160,77 @@ public int Foo() return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task BlockBodiedMethod_LocalInIfCondition() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + var threshold = Bar * 2; + if (threshold > 10) + { + return 1; + } + else + { + return 0; + } + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task BlockBodiedMethod_LocalInSwitchExpression() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public string Foo() + { + var value = Bar * 2; + switch (value) + { + case 2: + return ""Two""; + case 4: + return ""Four""; + default: + return ""Other""; + } + } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + [Fact] public Task BlockBodiedMethod_LocalsInNestedBlock_ProducesDiagnostic() { From b8af892b9b90d6343513a3ebdee11493ea391bbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:49:19 +0000 Subject: [PATCH 14/26] Improve error reporting for side effects in block-bodied methods - Add specific diagnostics (EFP0004, EFP0005) for side effects - Detect property assignments with clear error messages - Detect compound assignments (+=, -=, etc.) - Detect increment/decrement operators (++, --) - Warn about non-projectable method calls - Error messages now point to the exact problematic line - All 209 tests passing (102 net8.0 + 107 net10.0) Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- .../AnalyzerReleases.Unshipped.md | 2 + .../BlockStatementConverter.cs | 107 +++++++++++++++- .../Diagnostics.cs | 16 +++ .../ExpressionSyntaxRewriter.cs | 2 + ...mpoundAssignment_ReportsError.verified.txt | 3 + ...ncrementOperator_ReportsError.verified.txt | 3 + ...ableMethodCall_ReportsWarning.verified.txt | 3 + ...opertyAssignment_ReportsError.verified.txt | 3 + .../ProjectionExpressionGeneratorTests.cs | 117 ++++++++++++++++++ 9 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md index 4911eaa..c1b0078 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md +++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md @@ -4,3 +4,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- EFP0002 | Design | Error | EFP0003 | Design | Warning | +EFP0004 | Design | Error | Statement with side effects in block-bodied method +EFP0005 | Design | Warning | Potential side effect in block-bodied method diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index 5192612..55abd92 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -122,9 +122,21 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax return elseBody; } - // If there are any remaining non-if statements, they're not supported + // If there are any remaining non-if statements, try to convert them individually + // This will provide better error messages for unsupported statements if (remainingStatements.Count > 0) { + // Try converting each remaining statement - this will provide specific error messages + foreach (var stmt in remainingStatements) + { + var converted = TryConvertStatement(stmt, memberName); + if (converted == null) + { + return null; + } + } + + // If we got here but had non-if statements, they weren't properly handled ReportUnsupportedStatement(remainingStatements[0], memberName, "Only local variable declarations and if statements without else (with return) are supported before the final return statement"); return null; @@ -186,10 +198,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec return TryConvertStatements(blockStmt.Statements.ToList(), memberName); - case ExpressionStatementSyntax: - // Expression statements are generally not useful in expression trees - ReportUnsupportedStatement(statement, memberName, "Expression statements are not supported"); - return null; + case ExpressionStatementSyntax exprStmt: + // Expression statements may contain side effects - analyze them + return AnalyzeExpressionStatement(exprStmt, memberName); case LocalDeclarationStatementSyntax: // Local declarations should be handled before the return statement @@ -405,6 +416,92 @@ private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) return (ExpressionSyntax)rewriter.Visit(expression); } + private ExpressionSyntax? AnalyzeExpressionStatement(ExpressionStatementSyntax exprStmt, string memberName) + { + var expression = exprStmt.Expression; + + // Check for specific side effects + switch (expression) + { + case AssignmentExpressionSyntax assignment: + ReportSideEffect(assignment, GetAssignmentErrorMessage(assignment)); + return null; + + case PostfixUnaryExpressionSyntax postfix when + postfix.IsKind(SyntaxKind.PostIncrementExpression) || + postfix.IsKind(SyntaxKind.PostDecrementExpression): + ReportSideEffect(postfix, $"Increment/decrement operator '{postfix.OperatorToken.Text}' has side effects and cannot be used in projectable methods"); + return null; + + case PrefixUnaryExpressionSyntax prefix when + prefix.IsKind(SyntaxKind.PreIncrementExpression) || + prefix.IsKind(SyntaxKind.PreDecrementExpression): + ReportSideEffect(prefix, $"Increment/decrement operator '{prefix.OperatorToken.Text}' has side effects and cannot be used in projectable methods"); + return null; + + case InvocationExpressionSyntax invocation: + // Check if this is a potentially impure method call + var symbolInfo = _expressionRewriter.GetSemanticModel().GetSymbolInfo(invocation); + if (symbolInfo.Symbol is IMethodSymbol methodSymbol) + { + // Check if method has [Projectable] attribute - those are safe + var hasProjectableAttr = methodSymbol.GetAttributes() + .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute"); + + if (!hasProjectableAttr) + { + ReportPotentialSideEffect(invocation, + $"Method call '{methodSymbol.Name}' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods"); + return null; + } + } + break; + } + + // If we got here, it's an expression statement we don't support + ReportUnsupportedStatement(exprStmt, memberName, "Expression statements are not supported in projectable methods"); + return null; + } + + private string GetAssignmentErrorMessage(AssignmentExpressionSyntax assignment) + { + var operatorText = assignment.OperatorToken.Text; + + if (assignment.IsKind(SyntaxKind.SimpleAssignmentExpression)) + { + if (assignment.Left is MemberAccessExpressionSyntax memberAccess) + { + return $"Property assignment '{memberAccess.Name}' has side effects and cannot be used in projectable methods"; + } + return $"Assignment operation has side effects and cannot be used in projectable methods"; + } + else + { + // Compound assignment like +=, -=, etc. + return $"Compound assignment operator '{operatorText}' has side effects and cannot be used in projectable methods"; + } + } + + private void ReportSideEffect(SyntaxNode node, string message) + { + var diagnostic = Diagnostic.Create( + Diagnostics.SideEffectInBlockBody, + node.GetLocation(), + message + ); + _context.ReportDiagnostic(diagnostic); + } + + private void ReportPotentialSideEffect(SyntaxNode node, string message) + { + var diagnostic = Diagnostic.Create( + Diagnostics.PotentialSideEffectInBlockBody, + node.GetLocation(), + message + ); + _context.ReportDiagnostic(diagnostic); + } + private void ReportUnsupportedStatement(StatementSyntax statement, string memberName, string reason) { var diagnostic = Diagnostic.Create( diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs index d98a1b8..6bcfaf1 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs @@ -33,5 +33,21 @@ public static class Diagnostics DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor SideEffectInBlockBody = new DiagnosticDescriptor( + id: "EFP0004", + title: "Statement with side effects in block-bodied method", + messageFormat: "{0}", + category: "Design", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor PotentialSideEffectInBlockBody = new DiagnosticDescriptor( + id: "EFP0005", + title: "Potential side effect in block-bodied method", + messageFormat: "{0}", + category: "Design", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index ef46396..f953701 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -26,6 +26,8 @@ public ExpressionSyntaxRewriter(INamedTypeSymbol targetTypeSymbol, NullCondition _extensionParameterName = extensionParameterName; } + public SemanticModel GetSemanticModel() => _semanticModel; + private SyntaxNode? VisitThisBaseExpression(CSharpSyntaxNode node) { // Swap out the use of this and base to @this and keep leading and trailing trivias diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt new file mode 100644 index 0000000..a6b0b53 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_CompoundAssignment_ReportsError.verified.txt @@ -0,0 +1,3 @@ +[ + (11,13): error EFP0004: Compound assignment operator '+=' has side effects and cannot be used in projectable methods +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt new file mode 100644 index 0000000..d47a3ba --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_IncrementOperator_ReportsError.verified.txt @@ -0,0 +1,3 @@ +[ + (12,13): error EFP0004: Increment/decrement operator '++' has side effects and cannot be used in projectable methods +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt new file mode 100644 index 0000000..26e6a19 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning.verified.txt @@ -0,0 +1,3 @@ +[ + (11,13): warning EFP0005: Method call 'WriteLine' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods +] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt new file mode 100644 index 0000000..e684d40 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_PropertyAssignment_ReportsError.verified.txt @@ -0,0 +1,3 @@ +[ + (11,13): error EFP0004: Assignment operation has side effects and cannot be used in projectable methods +] \ 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 3254530..ae09af6 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2492,6 +2492,123 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task BlockBodiedMethod_PropertyAssignment_ReportsError() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + Bar = 10; + return Bar; + } + } +} +", expectedToCompile: true); + + var result = RunGenerator(compilation); + + // Should have a diagnostic about side effects + Assert.NotEmpty(result.Diagnostics); + Assert.Contains(result.Diagnostics, d => d.Id == "EFP0004"); + + return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); + } + + [Fact] + public Task BlockBodiedMethod_CompoundAssignment_ReportsError() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + Bar += 10; + return Bar; + } + } +} +", expectedToCompile: true); + + var result = RunGenerator(compilation); + + // Should have a diagnostic about side effects + Assert.NotEmpty(result.Diagnostics); + Assert.Contains(result.Diagnostics, d => d.Id == "EFP0004"); + + return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); + } + + [Fact] + public Task BlockBodiedMethod_IncrementOperator_ReportsError() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + var x = 5; + x++; + return x; + } + } +} +", expectedToCompile: true); + + var result = RunGenerator(compilation); + + // Should have a diagnostic about side effects + Assert.NotEmpty(result.Diagnostics); + Assert.Contains(result.Diagnostics, d => d.Id == "EFP0004"); + + return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); + } + + [Fact] + public Task BlockBodiedMethod_NonProjectableMethodCall_ReportsWarning() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo() + { + Console.WriteLine(""test""); + return Bar; + } + } +} +", expectedToCompile: true); + + var result = RunGenerator(compilation); + + // Should have a diagnostic about potential side effects + Assert.NotEmpty(result.Diagnostics); + Assert.Contains(result.Diagnostics, d => d.Id == "EFP0005"); + + return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); + } + [Fact] public Task MethodOverloads_WithDifferentParameterTypes() { From b697cd21f1f3f6b622634ddf31ca87f49ebac040 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 07:50:15 +0000 Subject: [PATCH 15/26] Add documentation for side effect detection - Document all detected side effects with examples - Show before/after comparison of error messages - Explain diagnostic codes EFP0004 and EFP0005 - Provide clear guidance for developers Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- docs/SideEffectDetection.md | 110 ++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/SideEffectDetection.md diff --git a/docs/SideEffectDetection.md b/docs/SideEffectDetection.md new file mode 100644 index 0000000..891294e --- /dev/null +++ b/docs/SideEffectDetection.md @@ -0,0 +1,110 @@ +# Side Effect Detection in Block-Bodied Methods + +This document describes the improved error reporting for side effects in block-bodied projectable methods. + +## Overview + +When using block-bodied methods with the `[Projectable]` attribute, the source generator now provides specific error messages that point to the exact line where side effects occur, making it much easier to identify and fix issues. + +## Detected Side Effects + +### 1. Property Assignments (EFP0004 - Error) + +**Code:** +```csharp +[Projectable] +public int Foo() +{ + Bar = 10; // ❌ Error on this line + return Bar; +} +``` + +**Error Message:** +``` +(11,13): error EFP0004: Assignment operation has side effects and cannot be used in projectable methods +``` + +### 2. Compound Assignments (EFP0004 - Error) + +**Code:** +```csharp +[Projectable] +public int Foo() +{ + Bar += 10; // ❌ Error on this line + return Bar; +} +``` + +**Error Message:** +``` +(11,13): error EFP0004: Compound assignment operator '+=' has side effects and cannot be used in projectable methods +``` + +### 3. Increment/Decrement Operators (EFP0004 - Error) + +**Code:** +```csharp +[Projectable] +public int Foo() +{ + var x = 5; + x++; // ❌ Error on this line + return x; +} +``` + +**Error Message:** +``` +(12,13): error EFP0004: Increment/decrement operator '++' has side effects and cannot be used in projectable methods +``` + +### 4. Non-Projectable Method Calls (EFP0005 - Warning) + +**Code:** +```csharp +[Projectable] +public int Foo() +{ + Console.WriteLine("test"); // ⚠️ Warning on this line + return Bar; +} +``` + +**Warning Message:** +``` +(11,13): warning EFP0005: Method call 'WriteLine' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods +``` + +## Before vs After + +### Before +Generic error message at the beginning of the method: +``` +warning EFP0003: Method 'Foo' contains an unsupported statement: Expression statements are not supported +``` + +### After +Specific error message pointing to the exact problematic line: +``` +error EFP0004: Property assignment 'Bar' has side effects and cannot be used in projectable methods +``` + +## Benefits + +1. **Precise Location**: Error messages now point to the exact line containing the side effect +2. **Specific Messages**: Clear explanation of what kind of side effect was detected +3. **Better Developer Experience**: Easier to identify and fix issues +4. **Severity Levels**: Errors for definite side effects, warnings for potential ones +5. **Actionable Guidance**: Messages explain why the code is problematic + +## Diagnostic Codes + +- **EFP0004**: Statement with side effects in block-bodied method (Error) +- **EFP0005**: Potential side effect in block-bodied method (Warning) + +These are in addition to the existing: +- **EFP0001**: Method or property should expose an expression body definition (Error) +- **EFP0002**: Method or property is not configured to support null-conditional expressions (Error) +- **EFP0003**: Unsupported statement in block-bodied method (Warning) From f2a805e6311ef08b6316b050de3585baea9f30f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:03:42 +0000 Subject: [PATCH 16/26] Initial exploration - understand pattern matching crash issue Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- ...iedMethod_WithPatternMatching.verified.txt | 17 +++++++++ .../ProjectionExpressionGeneratorTests.cs | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt 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..75eb6dd --- /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 is { IsActive: true, Value: > 100 } ? "Active High" : "Other"; + } + } +} \ 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 ae09af6..52ff32d 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2609,6 +2609,41 @@ public int Foo() return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); } + [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] + public static string GetComplexCategory(this Entity entity) + { + if (entity is { IsActive: true, Value: > 100 }) + { + return ""Active High""; + } + return ""Other""; + } + } +} +", expectedToCompile: true); + + 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 MethodOverloads_WithDifferentParameterTypes() { From 31f42673e3a612cb7c3e2c9a023fdd4686be7ab5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:06:29 +0000 Subject: [PATCH 17/26] Fix pattern matching support in block-bodied methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add VisitIsPatternExpression to ExpressionSyntaxRewriter - Convert pattern matching to equivalent expressions: - RecursivePattern: entity is { Prop: value } → null check + property checks - RelationalPattern: value is > 100 → value > 100 - ConstantPattern: entity is null → entity == null - UnaryPattern: entity is not null → !(entity == null) - Add comprehensive tests for all pattern types - All 217 tests passing (106 net8.0 + 111 net10.0) Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- .../ExpressionSyntaxRewriter.cs | 128 ++++++++++++++++++ ...iedMethod_WithConstantPattern.verified.txt | 17 +++ ...ckBodiedMethod_WithNotPattern.verified.txt | 17 +++ ...iedMethod_WithPatternMatching.verified.txt | 2 +- ...dMethod_WithRelationalPattern.verified.txt | 17 +++ .../ProjectionExpressionGeneratorTests.cs | 99 ++++++++++++++ 6 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index f953701..a73ca06 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -676,5 +679,130 @@ 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); + var convertedPattern = ConvertPatternToExpression(node.Pattern, expression); + + return convertedPattern; + } + + private ExpressionSyntax ConvertPatternToExpression(PatternSyntax pattern, ExpressionSyntax expression) + { + switch (pattern) + { + case RecursivePatternSyntax recursivePattern: + return ConvertRecursivePattern(recursivePattern, expression); + + case ConstantPatternSyntax constantPattern: + // e is null or e is 5 + return SyntaxFactory.BinaryExpression( + SyntaxKind.EqualsExpression, + expression, + (ExpressionSyntax)Visit(constantPattern.Expression) + ); + + case DeclarationPatternSyntax declarationPattern: + // e is string s -> e is string (type check) + return SyntaxFactory.BinaryExpression( + SyntaxKind.IsExpression, + expression, + declarationPattern.Type + ); + + case RelationalPatternSyntax relationalPattern: + // e is > 100 + var binaryKind = relationalPattern.OperatorToken.Kind() switch + { + SyntaxKind.LessThanToken => SyntaxKind.LessThanExpression, + SyntaxKind.LessThanEqualsToken => SyntaxKind.LessThanOrEqualExpression, + SyntaxKind.GreaterThanToken => SyntaxKind.GreaterThanExpression, + SyntaxKind.GreaterThanEqualsToken => SyntaxKind.GreaterThanOrEqualExpression, + _ => throw new NotSupportedException($"Relational operator {relationalPattern.OperatorToken} not supported") + }; + + return SyntaxFactory.BinaryExpression( + binaryKind, + 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); + + var logicalKind = binaryPattern.OperatorToken.Kind() switch + { + SyntaxKind.AndKeyword => SyntaxKind.LogicalAndExpression, + SyntaxKind.OrKeyword => SyntaxKind.LogicalOrExpression, + _ => throw new NotSupportedException($"Binary pattern operator {binaryPattern.OperatorToken} not supported") + }; + + return SyntaxFactory.BinaryExpression(logicalKind, left, right); + + case UnaryPatternSyntax unaryPattern when unaryPattern.OperatorToken.IsKind(SyntaxKind.NotKeyword): + // e is not null + var innerPattern = ConvertPatternToExpression(unaryPattern.Pattern, expression); + return SyntaxFactory.PrefixUnaryExpression( + SyntaxKind.LogicalNotExpression, + SyntaxFactory.ParenthesizedExpression(innerPattern) + ); + + default: + throw new NotSupportedException($"Pattern type {pattern.GetType().Name} is not yet supported in projectable methods"); + } + } + + private ExpressionSyntax ConvertRecursivePattern(RecursivePatternSyntax recursivePattern, ExpressionSyntax expression) + { + // entity is { IsActive: true, Value: > 100 } + // Convert to: entity != null && entity.IsActive == true && entity.Value > 100 + + var conditions = new List(); + + // Add null check first (unless pattern explicitly includes null) + var nullCheck = SyntaxFactory.BinaryExpression( + SyntaxKind.NotEqualsExpression, + expression, + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + ); + conditions.Add(nullCheck); + + // Handle property patterns + if (recursivePattern.PropertyPatternClause != null) + { + foreach (var subpattern in recursivePattern.PropertyPatternClause.Subpatterns) + { + var memberAccess = subpattern.NameColon != null + ? SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + expression, + SyntaxFactory.IdentifierName(subpattern.NameColon.Name.Identifier) + ) + : expression; + + var condition = ConvertPatternToExpression(subpattern.Pattern, memberAccess); + conditions.Add(condition); + } + } + + // Combine all conditions with && + var result = conditions[0]; + for (int 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_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_WithPatternMatching.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt index 75eb6dd..a11076d 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt @@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated { static global::System.Linq.Expressions.Expression> Expression() { - return (global::Foo.Entity entity) => entity is { IsActive: true, Value: > 100 } ? "Active High" : "Other"; + 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.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index 52ff32d..bab5608 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2644,6 +2644,105 @@ public static string GetComplexCategory(this Entity entity) 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] + public static string GetCategory(this Entity entity) + { + if (entity.Value is > 100) + { + return ""High""; + } + return ""Low""; + } + } +} +", expectedToCompile: true); + + 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] + public static bool IsNull(this Entity entity) + { + if (entity is null) + { + return true; + } + return false; + } + } +} +", expectedToCompile: true); + + 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] + public static bool IsNotNull(this Entity entity) + { + if (entity is not null) + { + return true; + } + return false; + } + } +} +", expectedToCompile: true); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + [Fact] public Task MethodOverloads_WithDifferentParameterTypes() { From adc95f5c24892aad5c2930d3f2e97f81b20438da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 08:07:04 +0000 Subject: [PATCH 18/26] Add documentation for pattern matching support - Document all supported pattern types with examples - Explain conversion logic and benefits - Provide complex examples showing nested patterns - Document limitations and error handling Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- docs/PatternMatchingSupport.md | 195 +++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 docs/PatternMatchingSupport.md diff --git a/docs/PatternMatchingSupport.md b/docs/PatternMatchingSupport.md new file mode 100644 index 0000000..e81be3c --- /dev/null +++ b/docs/PatternMatchingSupport.md @@ -0,0 +1,195 @@ +# Pattern Matching Support in Block-Bodied Methods + +This document describes how pattern matching is handled in block-bodied projectable methods. + +## Overview + +C# pattern matching (the `is` operator with patterns) is not supported in expression trees and will cause a CS8122 compilation error. The source generator automatically converts pattern matching syntax into equivalent boolean expressions that work in expression trees. + +## Supported Pattern Types + +### 1. Recursive Patterns (Property Patterns) + +**Syntax:** +```csharp +[Projectable] +public static string GetCategory(this Entity entity) +{ + if (entity is { IsActive: true, Value: > 100 }) + { + return "Active High"; + } + return "Other"; +} +``` + +**Converted To:** +```csharp +entity != null && entity.IsActive == true && entity.Value > 100 ? "Active High" : "Other" +``` + +The pattern is converted to: +1. Null check: `entity != null` +2. Property checks: `entity.IsActive == true && entity.Value > 100` +3. Combined with logical AND + +### 2. Relational Patterns + +**Syntax:** +```csharp +[Projectable] +public static string GetCategory(this Entity entity) +{ + if (entity.Value is > 100) + { + return "High"; + } + return "Low"; +} +``` + +**Converted To:** +```csharp +entity.Value > 100 ? "High" : "Low" +``` + +Supported relational operators: +- `>` (greater than) +- `>=` (greater than or equal) +- `<` (less than) +- `<=` (less than or equal) + +### 3. Constant Patterns + +**Syntax:** +```csharp +[Projectable] +public static bool IsNull(this Entity entity) +{ + if (entity is null) + { + return true; + } + return false; +} +``` + +**Converted To:** +```csharp +entity == null ? true : false +``` + +### 4. Unary Patterns (Not Patterns) + +**Syntax:** +```csharp +[Projectable] +public static bool IsNotNull(this Entity entity) +{ + if (entity is not null) + { + return true; + } + return false; +} +``` + +**Converted To:** +```csharp +!(entity == null) ? true : false +``` + +### 5. Binary Patterns (And/Or) + +**Syntax:** +```csharp +[Projectable] +public static bool IsInRange(this Entity entity) +{ + if (entity.Value is > 10 and < 100) + { + return true; + } + return false; +} +``` + +**Converted To:** +```csharp +entity.Value > 10 && entity.Value < 100 ? true : false +``` + +## Benefits + +1. **Modern C# Syntax**: Use pattern matching in block-bodied methods just like regular C# code +2. **Automatic Conversion**: No manual rewriting needed - the generator handles it +3. **Expression Tree Compatibility**: Generated code compiles without CS8122 errors +4. **Semantic Equivalence**: Converted expressions maintain the same behavior as patterns + +## Limitations + +Not all pattern types are currently supported: +- Type patterns with variable declarations may have limited support +- List patterns are not yet supported +- Some complex nested patterns may not be supported + +If you encounter an unsupported pattern, you'll receive an error message indicating which pattern type is not supported. + +## Examples + +### Complex Property Pattern +```csharp +[Projectable] +public static string GetStatus(this Order order) +{ + if (order is { Status: "Completed", Amount: > 1000, Customer.IsVip: true }) + { + return "VIP High Value Completed"; + } + return "Other"; +} +``` + +**Generates:** +```csharp +order != null && +order.Status == "Completed" && +order.Amount > 1000 && +order.Customer.IsVip == true + ? "VIP High Value Completed" + : "Other" +``` + +### Range Check with Relational Patterns +```csharp +[Projectable] +public static string GetRange(this Entity entity) +{ + if (entity.Value is >= 0 and < 50) + { + return "Low"; + } + else if (entity.Value is >= 50 and < 100) + { + return "Medium"; + } + return "High"; +} +``` + +**Generates:** +```csharp +entity.Value >= 0 && entity.Value < 50 ? "Low" : +entity.Value >= 50 && entity.Value < 100 ? "Medium" : +"High" +``` + +## Technical Details + +The conversion is implemented in `ExpressionSyntaxRewriter.VisitIsPatternExpression` which: +1. Visits the expression being tested +2. Converts the pattern to an equivalent expression using `ConvertPatternToExpression` +3. Handles nested patterns recursively +4. Combines multiple property checks with logical AND operators + +This ensures that all pattern matching is transformed into expression tree-compatible code before code generation. From 5f83f40600ea160393895809f5a8f3bef90aef59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:32:20 +0000 Subject: [PATCH 19/26] Revert pattern matching commits to separate feature Pattern matching support has been moved to a separate branch/PR. This PR now focuses solely on block-bodied method support: - If-else statements - Switch statements - Local variables - Side effect detection Reverted commits: - adc95f5: Add documentation for pattern matching support - 31f4267: Fix pattern matching support in block-bodied methods - f2a805e: Initial exploration - understand pattern matching crash issue All 209 tests passing (102 net8.0 + 107 net10.0 generator tests, plus functional and unit tests) Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- docs/PatternMatchingSupport.md | 195 ------------------ .../ExpressionSyntaxRewriter.cs | 128 ------------ ...iedMethod_WithConstantPattern.verified.txt | 17 -- ...ckBodiedMethod_WithNotPattern.verified.txt | 17 -- ...iedMethod_WithPatternMatching.verified.txt | 17 -- ...dMethod_WithRelationalPattern.verified.txt | 17 -- .../ProjectionExpressionGeneratorTests.cs | 134 ------------ 7 files changed, 525 deletions(-) delete mode 100644 docs/PatternMatchingSupport.md delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt delete mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt diff --git a/docs/PatternMatchingSupport.md b/docs/PatternMatchingSupport.md deleted file mode 100644 index e81be3c..0000000 --- a/docs/PatternMatchingSupport.md +++ /dev/null @@ -1,195 +0,0 @@ -# Pattern Matching Support in Block-Bodied Methods - -This document describes how pattern matching is handled in block-bodied projectable methods. - -## Overview - -C# pattern matching (the `is` operator with patterns) is not supported in expression trees and will cause a CS8122 compilation error. The source generator automatically converts pattern matching syntax into equivalent boolean expressions that work in expression trees. - -## Supported Pattern Types - -### 1. Recursive Patterns (Property Patterns) - -**Syntax:** -```csharp -[Projectable] -public static string GetCategory(this Entity entity) -{ - if (entity is { IsActive: true, Value: > 100 }) - { - return "Active High"; - } - return "Other"; -} -``` - -**Converted To:** -```csharp -entity != null && entity.IsActive == true && entity.Value > 100 ? "Active High" : "Other" -``` - -The pattern is converted to: -1. Null check: `entity != null` -2. Property checks: `entity.IsActive == true && entity.Value > 100` -3. Combined with logical AND - -### 2. Relational Patterns - -**Syntax:** -```csharp -[Projectable] -public static string GetCategory(this Entity entity) -{ - if (entity.Value is > 100) - { - return "High"; - } - return "Low"; -} -``` - -**Converted To:** -```csharp -entity.Value > 100 ? "High" : "Low" -``` - -Supported relational operators: -- `>` (greater than) -- `>=` (greater than or equal) -- `<` (less than) -- `<=` (less than or equal) - -### 3. Constant Patterns - -**Syntax:** -```csharp -[Projectable] -public static bool IsNull(this Entity entity) -{ - if (entity is null) - { - return true; - } - return false; -} -``` - -**Converted To:** -```csharp -entity == null ? true : false -``` - -### 4. Unary Patterns (Not Patterns) - -**Syntax:** -```csharp -[Projectable] -public static bool IsNotNull(this Entity entity) -{ - if (entity is not null) - { - return true; - } - return false; -} -``` - -**Converted To:** -```csharp -!(entity == null) ? true : false -``` - -### 5. Binary Patterns (And/Or) - -**Syntax:** -```csharp -[Projectable] -public static bool IsInRange(this Entity entity) -{ - if (entity.Value is > 10 and < 100) - { - return true; - } - return false; -} -``` - -**Converted To:** -```csharp -entity.Value > 10 && entity.Value < 100 ? true : false -``` - -## Benefits - -1. **Modern C# Syntax**: Use pattern matching in block-bodied methods just like regular C# code -2. **Automatic Conversion**: No manual rewriting needed - the generator handles it -3. **Expression Tree Compatibility**: Generated code compiles without CS8122 errors -4. **Semantic Equivalence**: Converted expressions maintain the same behavior as patterns - -## Limitations - -Not all pattern types are currently supported: -- Type patterns with variable declarations may have limited support -- List patterns are not yet supported -- Some complex nested patterns may not be supported - -If you encounter an unsupported pattern, you'll receive an error message indicating which pattern type is not supported. - -## Examples - -### Complex Property Pattern -```csharp -[Projectable] -public static string GetStatus(this Order order) -{ - if (order is { Status: "Completed", Amount: > 1000, Customer.IsVip: true }) - { - return "VIP High Value Completed"; - } - return "Other"; -} -``` - -**Generates:** -```csharp -order != null && -order.Status == "Completed" && -order.Amount > 1000 && -order.Customer.IsVip == true - ? "VIP High Value Completed" - : "Other" -``` - -### Range Check with Relational Patterns -```csharp -[Projectable] -public static string GetRange(this Entity entity) -{ - if (entity.Value is >= 0 and < 50) - { - return "Low"; - } - else if (entity.Value is >= 50 and < 100) - { - return "Medium"; - } - return "High"; -} -``` - -**Generates:** -```csharp -entity.Value >= 0 && entity.Value < 50 ? "Low" : -entity.Value >= 50 && entity.Value < 100 ? "Medium" : -"High" -``` - -## Technical Details - -The conversion is implemented in `ExpressionSyntaxRewriter.VisitIsPatternExpression` which: -1. Visits the expression being tested -2. Converts the pattern to an equivalent expression using `ConvertPatternToExpression` -3. Handles nested patterns recursively -4. Combines multiple property checks with logical AND operators - -This ensures that all pattern matching is transformed into expression tree-compatible code before code generation. diff --git a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs index a73ca06..f953701 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ExpressionSyntaxRewriter.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -679,130 +676,5 @@ 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); - var convertedPattern = ConvertPatternToExpression(node.Pattern, expression); - - return convertedPattern; - } - - private ExpressionSyntax ConvertPatternToExpression(PatternSyntax pattern, ExpressionSyntax expression) - { - switch (pattern) - { - case RecursivePatternSyntax recursivePattern: - return ConvertRecursivePattern(recursivePattern, expression); - - case ConstantPatternSyntax constantPattern: - // e is null or e is 5 - return SyntaxFactory.BinaryExpression( - SyntaxKind.EqualsExpression, - expression, - (ExpressionSyntax)Visit(constantPattern.Expression) - ); - - case DeclarationPatternSyntax declarationPattern: - // e is string s -> e is string (type check) - return SyntaxFactory.BinaryExpression( - SyntaxKind.IsExpression, - expression, - declarationPattern.Type - ); - - case RelationalPatternSyntax relationalPattern: - // e is > 100 - var binaryKind = relationalPattern.OperatorToken.Kind() switch - { - SyntaxKind.LessThanToken => SyntaxKind.LessThanExpression, - SyntaxKind.LessThanEqualsToken => SyntaxKind.LessThanOrEqualExpression, - SyntaxKind.GreaterThanToken => SyntaxKind.GreaterThanExpression, - SyntaxKind.GreaterThanEqualsToken => SyntaxKind.GreaterThanOrEqualExpression, - _ => throw new NotSupportedException($"Relational operator {relationalPattern.OperatorToken} not supported") - }; - - return SyntaxFactory.BinaryExpression( - binaryKind, - 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); - - var logicalKind = binaryPattern.OperatorToken.Kind() switch - { - SyntaxKind.AndKeyword => SyntaxKind.LogicalAndExpression, - SyntaxKind.OrKeyword => SyntaxKind.LogicalOrExpression, - _ => throw new NotSupportedException($"Binary pattern operator {binaryPattern.OperatorToken} not supported") - }; - - return SyntaxFactory.BinaryExpression(logicalKind, left, right); - - case UnaryPatternSyntax unaryPattern when unaryPattern.OperatorToken.IsKind(SyntaxKind.NotKeyword): - // e is not null - var innerPattern = ConvertPatternToExpression(unaryPattern.Pattern, expression); - return SyntaxFactory.PrefixUnaryExpression( - SyntaxKind.LogicalNotExpression, - SyntaxFactory.ParenthesizedExpression(innerPattern) - ); - - default: - throw new NotSupportedException($"Pattern type {pattern.GetType().Name} is not yet supported in projectable methods"); - } - } - - private ExpressionSyntax ConvertRecursivePattern(RecursivePatternSyntax recursivePattern, ExpressionSyntax expression) - { - // entity is { IsActive: true, Value: > 100 } - // Convert to: entity != null && entity.IsActive == true && entity.Value > 100 - - var conditions = new List(); - - // Add null check first (unless pattern explicitly includes null) - var nullCheck = SyntaxFactory.BinaryExpression( - SyntaxKind.NotEqualsExpression, - expression, - SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) - ); - conditions.Add(nullCheck); - - // Handle property patterns - if (recursivePattern.PropertyPatternClause != null) - { - foreach (var subpattern in recursivePattern.PropertyPatternClause.Subpatterns) - { - var memberAccess = subpattern.NameColon != null - ? SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - expression, - SyntaxFactory.IdentifierName(subpattern.NameColon.Name.Identifier) - ) - : expression; - - var condition = ConvertPatternToExpression(subpattern.Pattern, memberAccess); - conditions.Add(condition); - } - } - - // Combine all conditions with && - var result = conditions[0]; - for (int 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_WithConstantPattern.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt deleted file mode 100644 index 6356921..0000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithConstantPattern.verified.txt +++ /dev/null @@ -1,17 +0,0 @@ -// -#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 deleted file mode 100644 index 797a367..0000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithNotPattern.verified.txt +++ /dev/null @@ -1,17 +0,0 @@ -// -#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_WithPatternMatching.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt deleted file mode 100644 index a11076d..0000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithPatternMatching.verified.txt +++ /dev/null @@ -1,17 +0,0 @@ -// -#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 deleted file mode 100644 index 55dcb0a..0000000 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithRelationalPattern.verified.txt +++ /dev/null @@ -1,17 +0,0 @@ -// -#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.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs index bab5608..ae09af6 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -2609,140 +2609,6 @@ public int Foo() return Verifier.Verify(result.Diagnostics.Select(d => d.ToString())); } - [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] - public static string GetComplexCategory(this Entity entity) - { - if (entity is { IsActive: true, Value: > 100 }) - { - return ""Active High""; - } - return ""Other""; - } - } -} -", expectedToCompile: true); - - 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] - public static string GetCategory(this Entity entity) - { - if (entity.Value is > 100) - { - return ""High""; - } - return ""Low""; - } - } -} -", expectedToCompile: true); - - 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] - public static bool IsNull(this Entity entity) - { - if (entity is null) - { - return true; - } - return false; - } - } -} -", expectedToCompile: true); - - 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] - public static bool IsNotNull(this Entity entity) - { - if (entity is not null) - { - return true; - } - return false; - } - } -} -", expectedToCompile: true); - - var result = RunGenerator(compilation); - - Assert.Empty(result.Diagnostics); - Assert.Single(result.GeneratedTrees); - - return Verifier.Verify(result.GeneratedTrees[0].ToString()); - } - [Fact] public Task MethodOverloads_WithDifferentParameterTypes() { From 8b144b5f416a2aaecc15a04dda500035f7d464d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:51:47 +0000 Subject: [PATCH 20/26] Address code review suggestions and update documentation Code Review Fixes: - Use proper type resolution for [Projectable] attribute check (SymbolEqualityComparer) - Add parentheses when inlining local variables to preserve operator precedence - Restrict multiple if-without-else pattern to simple return bodies Documentation Updates: - Add block-bodied methods FAQ section to README with examples - Merge SideEffectDetection.md content into BlockBodiedMethods.md - Remove standalone SideEffectDetection.md file - Link to BlockBodiedMethods.md from README All 209 tests passing Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com> --- README.md | 30 +++++ docs/BlockBodiedMethods.md | 82 +++++++++++++ docs/SideEffectDetection.md | 110 ------------------ .../BlockStatementConverter.cs | 66 ++++++++++- 4 files changed, 174 insertions(+), 114 deletions(-) delete mode 100644 docs/SideEffectDetection.md diff --git a/README.md b/README.md index ab688bf..d61a2ee 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,36 @@ GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') ``` +#### Can I use block-bodied methods instead of expression-bodied methods? +Yes! As of version 3.x, you can now use traditional block-bodied methods with `[Projectable]`. This makes code more readable when dealing with complex conditional logic: + +```csharp +// Expression-bodied (still supported) +[Projectable] +public string Level() => Value > 100 ? "High" : Value > 50 ? "Medium" : "Low"; + +// Block-bodied (now also supported!) +[Projectable] +public string Level() +{ + if (Value > 100) + return "High"; + else if (Value > 50) + return "Medium"; + else + return "Low"; +} +``` + +Both generate identical SQL. Block-bodied methods support: +- If-else statements (converted to ternary/CASE expressions) +- Switch statements +- Local variables (automatically inlined) +- Simple return statements + +The generator will also detect and report side effects (assignments, method calls to non-projectable methods, etc.) with precise error messages. See [Block-Bodied Methods Documentation](docs/BlockBodiedMethods.md) for complete details. + + #### How do I expand enum extension methods? When you have an enum property and want to call an extension method on it (like getting a display name from a `[Display]` attribute), you can use the `ExpandEnumMethods` property on the `[Projectable]` attribute. This will expand the enum method call into a chain of ternary expressions for each enum value, allowing EF Core to translate it to SQL CASE expressions. diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMethods.md index fe19c69..12c0022 100644 --- a/docs/BlockBodiedMethods.md +++ b/docs/BlockBodiedMethods.md @@ -299,3 +299,85 @@ SELECT CASE END FROM [Entity] AS [e] ``` + +## Side Effect Detection + +The generator provides specific error reporting for side effects in block-bodied methods, helping you identify and fix issues quickly. + +### Detected Side Effects + +#### 1. Property Assignments (EFP0004 - Error) + +Property assignments modify state and are not allowed: + +```csharp +[Projectable] +public int Foo() +{ + Bar = 10; // ❌ Error: Assignment operation has side effects + return Bar; +} +``` + +#### 2. Compound Assignments (EFP0004 - Error) + +Compound assignment operators like `+=`, `-=`, `*=`, etc. are not allowed: + +```csharp +[Projectable] +public int Foo() +{ + Bar += 10; // ❌ Error: Compound assignment operator '+=' has side effects + return Bar; +} +``` + +#### 3. Increment/Decrement Operators (EFP0004 - Error) + +Pre and post increment/decrement operators are not allowed: + +```csharp +[Projectable] +public int Foo() +{ + var x = 5; + x++; // ❌ Error: Increment/decrement operator '++' has side effects + return x; +} +``` + +#### 4. Non-Projectable Method Calls (EFP0005 - Warning) + +Calls to methods not marked with `[Projectable]` may have side effects: + +```csharp +[Projectable] +public int Foo() +{ + Console.WriteLine("test"); // ⚠️ Warning: Method call 'WriteLine' may have side effects + return Bar; +} +``` + +### Diagnostic Codes + +- **EFP0003**: Unsupported statement in block-bodied method (Warning) +- **EFP0004**: Statement with side effects in block-bodied method (Error) +- **EFP0005**: Potential side effect in block-bodied method (Warning) + +### Error Message Improvements + +Instead of generic error messages, you now get precise, actionable feedback: + +**Before:** +``` +warning EFP0003: Method 'Foo' contains an unsupported statement: Expression statements are not supported +``` + +**After:** +``` +error EFP0004: Property assignment 'Bar' has side effects and cannot be used in projectable methods +``` + +The error message points to the exact line with the problematic code, making it much easier to identify and fix issues. + diff --git a/docs/SideEffectDetection.md b/docs/SideEffectDetection.md deleted file mode 100644 index 891294e..0000000 --- a/docs/SideEffectDetection.md +++ /dev/null @@ -1,110 +0,0 @@ -# Side Effect Detection in Block-Bodied Methods - -This document describes the improved error reporting for side effects in block-bodied projectable methods. - -## Overview - -When using block-bodied methods with the `[Projectable]` attribute, the source generator now provides specific error messages that point to the exact line where side effects occur, making it much easier to identify and fix issues. - -## Detected Side Effects - -### 1. Property Assignments (EFP0004 - Error) - -**Code:** -```csharp -[Projectable] -public int Foo() -{ - Bar = 10; // ❌ Error on this line - return Bar; -} -``` - -**Error Message:** -``` -(11,13): error EFP0004: Assignment operation has side effects and cannot be used in projectable methods -``` - -### 2. Compound Assignments (EFP0004 - Error) - -**Code:** -```csharp -[Projectable] -public int Foo() -{ - Bar += 10; // ❌ Error on this line - return Bar; -} -``` - -**Error Message:** -``` -(11,13): error EFP0004: Compound assignment operator '+=' has side effects and cannot be used in projectable methods -``` - -### 3. Increment/Decrement Operators (EFP0004 - Error) - -**Code:** -```csharp -[Projectable] -public int Foo() -{ - var x = 5; - x++; // ❌ Error on this line - return x; -} -``` - -**Error Message:** -``` -(12,13): error EFP0004: Increment/decrement operator '++' has side effects and cannot be used in projectable methods -``` - -### 4. Non-Projectable Method Calls (EFP0005 - Warning) - -**Code:** -```csharp -[Projectable] -public int Foo() -{ - Console.WriteLine("test"); // ⚠️ Warning on this line - return Bar; -} -``` - -**Warning Message:** -``` -(11,13): warning EFP0005: Method call 'WriteLine' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods -``` - -## Before vs After - -### Before -Generic error message at the beginning of the method: -``` -warning EFP0003: Method 'Foo' contains an unsupported statement: Expression statements are not supported -``` - -### After -Specific error message pointing to the exact problematic line: -``` -error EFP0004: Property assignment 'Bar' has side effects and cannot be used in projectable methods -``` - -## Benefits - -1. **Precise Location**: Error messages now point to the exact line containing the side effect -2. **Specific Messages**: Clear explanation of what kind of side effect was detected -3. **Better Developer Experience**: Easier to identify and fix issues -4. **Severity Levels**: Errors for definite side effects, warnings for potential ones -5. **Actionable Guidance**: Messages explain why the code is problematic - -## Diagnostic Codes - -- **EFP0004**: Statement with side effects in block-bodied method (Error) -- **EFP0005**: Potential side effect in block-bodied method (Warning) - -These are in addition to the existing: -- **EFP0001**: Method or property should expose an expression body definition (Error) -- **EFP0002**: Method or property is not configured to support null-conditional expressions (Error) -- **EFP0003**: Unsupported statement in block-bodied method (Warning) diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index 55abd92..d392c9b 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -87,14 +87,27 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax } // Check if we have a pattern like multiple if statements without else followed by a final return: - // var x = ...; if (a) return 1; if (b) return 2; return 3; + // if (a) return 1; if (b) return 2; return 3; // This can be converted to nested ternaries: a ? 1 : (b ? 2 : 3) + // Each if statement must have a body that is a simple return statement if (lastStatement is ReturnStatementSyntax finalReturn && remainingStatements.All(s => s is IfStatementSyntax { Else: null })) { // All remaining non-return statements are if statements without else var ifStatements = remainingStatements.Cast().ToList(); + // Validate each if statement has a simple return statement body + foreach (var ifStmt in ifStatements) + { + if (!IsSimpleReturnBody(ifStmt.Statement)) + { + ReportUnsupportedStatement(ifStmt, memberName, + "Multiple if statements without else clauses require each if body to be a simple return statement. " + + "Complex if bodies are not supported in this pattern."); + return null; + } + } + // Start with the final return as the base expression var elseBody = TryConvertReturnStatement(finalReturn, memberName); if (elseBody == null) @@ -146,6 +159,19 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax return TryConvertStatement(lastStatement, memberName); } + /// + /// Checks if a statement is a simple return statement or a block containing only a return statement. + /// + private static bool IsSimpleReturnBody(StatementSyntax statement) + { + return statement switch + { + ReturnStatementSyntax => true, + BlockSyntax block => block.Statements.Count == 1 && block.Statements[0] is ReturnStatementSyntax, + _ => false + }; + } + private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDecl, string memberName) { foreach (var variable in localDecl.Declaration.Variables) @@ -445,8 +471,12 @@ private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) if (symbolInfo.Symbol is IMethodSymbol methodSymbol) { // Check if method has [Projectable] attribute - those are safe + // Use proper type resolution to avoid false positives + var semanticModel = _expressionRewriter.GetSemanticModel(); + var projectableAttributeType = semanticModel.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projectables.ProjectableAttribute"); + var hasProjectableAttr = methodSymbol.GetAttributes() - .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute"); + .Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, projectableAttributeType)); if (!hasProjectableAttr) { @@ -528,12 +558,40 @@ public LocalVariableReplacer(Dictionary localVariables var identifier = node.Identifier.Text; if (_localVariables.TryGetValue(identifier, out var replacement)) { - // Replace the identifier with the expression it was initialized with - return replacement.WithTriviaFrom(node); + // Replace the identifier with the expression it was initialized with. + // Wrap non-trivial expressions in parentheses to preserve operator precedence. + var replacementWithTrivia = replacement.WithTriviaFrom(node); + + if (RequiresParentheses(replacementWithTrivia)) + { + return SyntaxFactory.ParenthesizedExpression(replacementWithTrivia); + } + + return replacementWithTrivia; } return base.VisitIdentifierName(node); } + + private static bool RequiresParentheses(ExpressionSyntax replacement) + { + // Simple expressions do not need parentheses when inlined. + if (replacement is IdentifierNameSyntax + or ThisExpressionSyntax + or BaseExpressionSyntax + or LiteralExpressionSyntax + or ParenthesizedExpressionSyntax + or MemberAccessExpressionSyntax + or InvocationExpressionSyntax + or ElementAccessExpressionSyntax) + { + return false; + } + + // For all other (potentially non-trivial) expressions, use parentheses + // to avoid changing semantics due to operator precedence. + return true; + } } } } From 2b70d43f3f43577ac2c5fbd86963955de47e0726 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Mon, 16 Feb 2026 21:55:12 +0100 Subject: [PATCH 21/26] Update block bodied release number --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d61a2ee..2f06978 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') ``` #### Can I use block-bodied methods instead of expression-bodied methods? -Yes! As of version 3.x, you can now use traditional block-bodied methods with `[Projectable]`. This makes code more readable when dealing with complex conditional logic: +Yes! As of version 6.x, you can now use traditional block-bodied methods with `[Projectable]`. This makes code more readable when dealing with complex conditional logic: ```csharp // Expression-bodied (still supported) From b4989af1910bdb62ea5109ea717d7493641667a5 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Mon, 16 Feb 2026 22:03:44 +0100 Subject: [PATCH 22/26] Reveret change about if with single return --- .../BlockStatementConverter.cs | 66 ++----------------- 1 file changed, 4 insertions(+), 62 deletions(-) diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index d392c9b..55abd92 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -87,27 +87,14 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax } // Check if we have a pattern like multiple if statements without else followed by a final return: - // if (a) return 1; if (b) return 2; return 3; + // var x = ...; if (a) return 1; if (b) return 2; return 3; // This can be converted to nested ternaries: a ? 1 : (b ? 2 : 3) - // Each if statement must have a body that is a simple return statement if (lastStatement is ReturnStatementSyntax finalReturn && remainingStatements.All(s => s is IfStatementSyntax { Else: null })) { // All remaining non-return statements are if statements without else var ifStatements = remainingStatements.Cast().ToList(); - // Validate each if statement has a simple return statement body - foreach (var ifStmt in ifStatements) - { - if (!IsSimpleReturnBody(ifStmt.Statement)) - { - ReportUnsupportedStatement(ifStmt, memberName, - "Multiple if statements without else clauses require each if body to be a simple return statement. " + - "Complex if bodies are not supported in this pattern."); - return null; - } - } - // Start with the final return as the base expression var elseBody = TryConvertReturnStatement(finalReturn, memberName); if (elseBody == null) @@ -159,19 +146,6 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax return TryConvertStatement(lastStatement, memberName); } - /// - /// Checks if a statement is a simple return statement or a block containing only a return statement. - /// - private static bool IsSimpleReturnBody(StatementSyntax statement) - { - return statement switch - { - ReturnStatementSyntax => true, - BlockSyntax block => block.Statements.Count == 1 && block.Statements[0] is ReturnStatementSyntax, - _ => false - }; - } - private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDecl, string memberName) { foreach (var variable in localDecl.Declaration.Variables) @@ -471,12 +445,8 @@ private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) if (symbolInfo.Symbol is IMethodSymbol methodSymbol) { // Check if method has [Projectable] attribute - those are safe - // Use proper type resolution to avoid false positives - var semanticModel = _expressionRewriter.GetSemanticModel(); - var projectableAttributeType = semanticModel.Compilation.GetTypeByMetadataName("EntityFrameworkCore.Projectables.ProjectableAttribute"); - var hasProjectableAttr = methodSymbol.GetAttributes() - .Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, projectableAttributeType)); + .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute"); if (!hasProjectableAttr) { @@ -558,40 +528,12 @@ public LocalVariableReplacer(Dictionary localVariables var identifier = node.Identifier.Text; if (_localVariables.TryGetValue(identifier, out var replacement)) { - // Replace the identifier with the expression it was initialized with. - // Wrap non-trivial expressions in parentheses to preserve operator precedence. - var replacementWithTrivia = replacement.WithTriviaFrom(node); - - if (RequiresParentheses(replacementWithTrivia)) - { - return SyntaxFactory.ParenthesizedExpression(replacementWithTrivia); - } - - return replacementWithTrivia; + // Replace the identifier with the expression it was initialized with + return replacement.WithTriviaFrom(node); } return base.VisitIdentifierName(node); } - - private static bool RequiresParentheses(ExpressionSyntax replacement) - { - // Simple expressions do not need parentheses when inlined. - if (replacement is IdentifierNameSyntax - or ThisExpressionSyntax - or BaseExpressionSyntax - or LiteralExpressionSyntax - or ParenthesizedExpressionSyntax - or MemberAccessExpressionSyntax - or InvocationExpressionSyntax - or ElementAccessExpressionSyntax) - { - return false; - } - - // For all other (potentially non-trivial) expressions, use parentheses - // to avoid changing semantics due to operator precedence. - return true; - } } } } From a741a2770f14642c922e0aba6845d7ada4428a22 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 18 Feb 2026 12:14:40 +0100 Subject: [PATCH 23/26] Mark this new feature as experimental and allow explicit getters for properties --- README.md | 13 +- ...BodiedMethods.md => BlockBodiedMembers.md} | 29 +- .../ProjectableAttribute.cs | 9 + .../AnalyzerReleases.Shipped.md | 24 +- .../AnalyzerReleases.Unshipped.md | 9 +- .../Diagnostics.cs | 23 +- .../ProjectableInterpreter.cs | 89 +++++- .../BlockBodiedMethodTests.cs | 60 ++-- .../BlockBodyProjectableCallTest.cs | 34 +-- ...yWithExplicitExpressionGetter.verified.txt | 17 ++ ...opertyWithExplicitBlockGetter.verified.txt | 17 ++ ...yWithExplicitExpressionGetter.verified.txt | 17 ++ .../ProjectionExpressionGeneratorTests.cs | 289 +++++++++++++++--- 13 files changed, 501 insertions(+), 129 deletions(-) rename docs/{BlockBodiedMethods.md => BlockBodiedMembers.md} (89%) create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt diff --git a/README.md b/README.md index 2f06978..471c6f5 100644 --- a/README.md +++ b/README.md @@ -159,8 +159,9 @@ GROUP BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') ORDER BY (COALESCE("u"."FirstName", '') || ' ') || COALESCE("u"."LastName", '') ``` -#### Can I use block-bodied methods instead of expression-bodied methods? -Yes! As of version 6.x, you can now use traditional block-bodied methods with `[Projectable]`. This makes code more readable when dealing with complex conditional logic: +#### Can I use block-bodied members instead of expression-bodied members? + +Yes! As of version 6.x, you can now use traditional block-bodied members with `[Projectable]`. This makes code more readable when dealing with complex conditional logic: ```csharp // Expression-bodied (still supported) @@ -168,7 +169,7 @@ Yes! As of version 6.x, you can now use traditional block-bodied methods with `[ public string Level() => Value > 100 ? "High" : Value > 50 ? "Medium" : "Low"; // Block-bodied (now also supported!) -[Projectable] +[Projectable(AllowBlockBody = true)] // Note: AllowBlockBody is required to remove the warning for experimental feature usage public string Level() { if (Value > 100) @@ -180,13 +181,15 @@ public string Level() } ``` -Both generate identical SQL. Block-bodied methods support: +> This is an experimental feature and may have some limitations. Please refer to the documentation for details. + +Both generate identical SQL. Block-bodied members support: - If-else statements (converted to ternary/CASE expressions) - Switch statements - Local variables (automatically inlined) - Simple return statements -The generator will also detect and report side effects (assignments, method calls to non-projectable methods, etc.) with precise error messages. See [Block-Bodied Methods Documentation](docs/BlockBodiedMethods.md) for complete details. +The generator will also detect and report side effects (assignments, method calls to non-projectable members, etc.) with precise error messages. See [Block-Bodied Members Documentation](docs/BlockBodiedMembers.md) for complete details. #### How do I expand enum extension methods? diff --git a/docs/BlockBodiedMethods.md b/docs/BlockBodiedMembers.md similarity index 89% rename from docs/BlockBodiedMethods.md rename to docs/BlockBodiedMembers.md index 12c0022..63be89f 100644 --- a/docs/BlockBodiedMethods.md +++ b/docs/BlockBodiedMembers.md @@ -1,6 +1,33 @@ # Block-Bodied Methods Support -As of this version, EntityFrameworkCore.Projectables now supports "classic" block-bodied methods decorated with `[Projectable]`, in addition to expression-bodied methods. +EntityFrameworkCore.Projectables now supports "classic" block-bodied members (methods and properties) decorated with `[Projectable]`, in addition to expression-bodied members. + +## ⚠️ Experimental Feature + +Block-bodied members support is currently **experimental**. By default, using a block-bodied member with `[Projectable]` will emit a warning: + +``` +EFP0001: Block-bodied member 'MethodName' is using an experimental feature. Set AllowBlockBody = true on the Projectable attribute to suppress this warning. +``` + +To acknowledge that you're using an experimental feature and suppress the warning, set `AllowBlockBody = true`: + +```csharp +[Projectable(AllowBlockBody = true)] +public string GetCategory() +{ + if (Value > 100) + { + return "High"; + } + else + { + return "Low"; + } +} +``` + +This requirement will be removed in a future version once the feature is considered stable. ## What's Supported diff --git a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs index 30af683..94b63b2 100644 --- a/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs +++ b/src/EntityFrameworkCore.Projectables.Abstractions/ProjectableAttribute.cs @@ -38,5 +38,14 @@ public sealed class ProjectableAttribute : Attribute /// /// public bool ExpandEnumMethods { get; set; } + + /// + /// Get or set whether to allow block-bodied members (experimental feature). + /// + /// + /// Block-bodied method support is experimental and may have limitations. + /// Set this to true to suppress the experimental feature warning. + /// + public bool AllowBlockBody { get; set; } } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md index 586c754..7bd9067 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md +++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md @@ -1,7 +1,25 @@ -## Release 5.0 +## Release 6.0 ### New Rules Rule ID | Category | Severity | Notes ---------|----------|----------|-------------------- -EFP0001 | Design | Error | +--------|----------|----------|--------------------------------------------------------------------------- +EFP0003 | Design | Warning | Unsupported statement in block-bodied method +EFP0004 | Design | Error | Statement with side effects in block-bodied method +EFP0005 | Design | Warning | Potential side effect in block-bodied method +EFP0006 | Design | Error | Method or property should expose an body definition (block or expression) + +### Changed Rules + +Rule ID | New Category | New Severity | Old Category | Old Severity | Notes"; +--------|--------------|--------------|--------------|--------------|----------------------------------------------------------------- +EFP0001 | Design | Warning | Design | Error | Changed to warning for experimental block-bodied members support + +## Release 5.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------------------------------------------------------------------------------ +EFP0001 | Design | Error | Method or property should expose an expression body definition +EFP0002 | Design | Error | Method or property is not configured to support null-conditional expressions diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md index c1b0078..5f28270 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md +++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Unshipped.md @@ -1,8 +1 @@ -### New Rules - -Rule ID | Category | Severity | Notes ---------|----------|----------|-------------------- -EFP0002 | Design | Error | -EFP0003 | Design | Warning | -EFP0004 | Design | Error | Statement with side effects in block-bodied method -EFP0005 | Design | Warning | Potential side effect in block-bodied method + \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs index 6bcfaf1..70e2964 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/Diagnostics.cs @@ -1,20 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; namespace EntityFrameworkCore.Projectables.Generator { public static class Diagnostics { - public static readonly DiagnosticDescriptor RequiresExpressionBodyDefinition = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor BlockBodyExperimental = 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", + title: "Block-bodied member support is experimental", + messageFormat: "Block-bodied member '{0}' is using an experimental feature. Set AllowBlockBody = true on the Projectable attribute to suppress this warning.", category: "Design", - DiagnosticSeverity.Error, + DiagnosticSeverity.Warning, isEnabledByDefault: true); public static readonly DiagnosticDescriptor NullConditionalRewriteUnsupported = new DiagnosticDescriptor( @@ -49,5 +44,13 @@ public static class Diagnostics DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor RequiresBodyDefinition = new DiagnosticDescriptor( + id: "EFP0006", + title: "Method or property should expose a body definition", + messageFormat: "Method or property '{0}' should expose a body definition (e.g. an expression-bodied member or a block-bodied method) to be used as the source for the generated expression tree.", + category: "Design", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + } } diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs index 5006081..356420b 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs @@ -80,6 +80,11 @@ static IEnumerable GetNestedInClassPathForExtensionMember(ITypeSymbol ex .Select(x => x.Value.Value is bool b && b) .FirstOrDefault(); + var allowBlockBody = projectableAttributeClass.NamedArguments + .Where(x => x.Key == "AllowBlockBody") + .Select(x => x.Value.Value is bool b && b) + .FirstOrDefault(); + var memberBody = member; if (useMemberBody is not null) @@ -124,18 +129,33 @@ x is IPropertySymbol xProperty && { return true; } - else if (x is PropertyDeclarationSyntax xProperty && - xProperty.ExpressionBody is not null) - { - return true; - } - else + else if (x is PropertyDeclarationSyntax xProperty) { - return false; + // Support expression-bodied properties: int Prop => value; + if (xProperty.ExpressionBody is not null) + { + return true; + } + + // Support properties with explicit getters: int Prop { get => value; } or { get { return value; } } + if (xProperty.AccessorList is not null) + { + var getter = xProperty.AccessorList.Accessors + .FirstOrDefault(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)); + if (getter?.ExpressionBody is not null || getter?.Body is not null) + { + return true; + } + } } + + return false; }); - if (memberBody is null) return null; + if (memberBody is null) + { + return null; + } } // Check if this member is inside a C# 14 extension block @@ -300,6 +320,7 @@ x is IPropertySymbol xProperty && descriptor.TargetNestedInClassNames = descriptor.NestedInClassNames; } + // Projectable methods if (memberBody is MethodDeclarationSyntax methodDeclarationSyntax) { ExpressionSyntax? bodyExpression = null; @@ -312,6 +333,14 @@ x is IPropertySymbol xProperty && else if (methodDeclarationSyntax.Body is not null) { // Block-bodied method (e.g., int Foo() { return 1; }) + + // Emit warning if AllowBlockBody is not set to true + if (!allowBlockBody) + { + var diagnostic = Diagnostic.Create(Diagnostics.BlockBodyExperimental, methodDeclarationSyntax.GetLocation(), memberSymbol.Name); + context.ReportDiagnostic(diagnostic); + } + var blockConverter = new BlockStatementConverter(context, expressionSyntaxRewriter); bodyExpression = blockConverter.TryConvertBlock(methodDeclarationSyntax.Body, memberSymbol.Name); @@ -325,7 +354,7 @@ x is IPropertySymbol xProperty && } else { - var diagnostic = Diagnostic.Create(Diagnostics.RequiresExpressionBodyDefinition, methodDeclarationSyntax.GetLocation(), memberSymbol.Name); + var diagnostic = Diagnostic.Create(Diagnostics.RequiresBodyDefinition, methodDeclarationSyntax.GetLocation(), memberSymbol.Name); context.ReportDiagnostic(diagnostic); return null; } @@ -360,11 +389,47 @@ x is IPropertySymbol xProperty && ); } } + + // Projectable properties else if (memberBody is PropertyDeclarationSyntax propertyDeclarationSyntax) { - if (propertyDeclarationSyntax.ExpressionBody is null) + ExpressionSyntax? bodyExpression = null; + + // Expression-bodied property: int Prop => value; + if (propertyDeclarationSyntax.ExpressionBody is not null) + { + + bodyExpression = propertyDeclarationSyntax.ExpressionBody.Expression; + } + else if (propertyDeclarationSyntax.AccessorList is not null) + { + // Property with explicit getter + var getter = propertyDeclarationSyntax.AccessorList.Accessors + .FirstOrDefault(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)); + + if (getter?.ExpressionBody is not null) + { + // get => expression; + bodyExpression = getter.ExpressionBody.Expression; + } + else if (getter?.Body is not null) + { + // get { return expression; } + // Emit warning if AllowBlockBody is not set to true + if (!allowBlockBody) + { + var diagnostic = Diagnostic.Create(Diagnostics.BlockBodyExperimental, propertyDeclarationSyntax.GetLocation(), memberSymbol.Name); + context.ReportDiagnostic(diagnostic); + } + + var blockConverter = new BlockStatementConverter(context, expressionSyntaxRewriter); + bodyExpression = blockConverter.TryConvertBlock(getter.Body, memberSymbol.Name); + } + } + + if (bodyExpression is null) { - var diagnostic = Diagnostic.Create(Diagnostics.RequiresExpressionBodyDefinition, propertyDeclarationSyntax.GetLocation(), memberSymbol.Name); + var diagnostic = Diagnostic.Create(Diagnostics.RequiresBodyDefinition, propertyDeclarationSyntax.GetLocation(), memberSymbol.Name); context.ReportDiagnostic(diagnostic); return null; } @@ -372,7 +437,7 @@ x is IPropertySymbol xProperty && var returnType = declarationSyntaxRewriter.Visit(propertyDeclarationSyntax.Type); descriptor.ReturnTypeName = returnType.ToString(); - descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(propertyDeclarationSyntax.ExpressionBody.Expression); + descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression); } else { diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs index 9622f34..98320b3 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodiedMethodTests.cs @@ -1,4 +1,4 @@ -using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; +using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using Microsoft.EntityFrameworkCore; using System.Linq; using System.Threading.Tasks; @@ -340,19 +340,19 @@ public Task ArithmeticInReturn_WorksCorrectly() public static class EntityExtensions { - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetConstant(this BlockBodiedMethodTests.Entity entity) { return 42; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetValuePlusTen(this BlockBodiedMethodTests.Entity entity) { return entity.Value + 10; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetCategory(this BlockBodiedMethodTests.Entity entity) { if (entity.Value > 100) @@ -365,7 +365,7 @@ public static string GetCategory(this BlockBodiedMethodTests.Entity entity) } } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetLevel(this BlockBodiedMethodTests.Entity entity) { if (entity.Value > 100) @@ -382,14 +382,14 @@ public static string GetLevel(this BlockBodiedMethodTests.Entity entity) } } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int CalculateDouble(this BlockBodiedMethodTests.Entity entity) { var doubled = entity.Value * 2; return doubled + 5; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetAdjustedValue(this BlockBodiedMethodTests.Entity entity) { if (entity.IsActive && entity.Value > 0) @@ -402,13 +402,13 @@ public static int GetAdjustedValue(this BlockBodiedMethodTests.Entity entity) } } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int Add(this BlockBodiedMethodTests.Entity entity, int a, int b) { return a + b; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int? GetPremiumIfActive(this BlockBodiedMethodTests.Entity entity) { if (entity.IsActive) @@ -418,7 +418,7 @@ public static int Add(this BlockBodiedMethodTests.Entity entity, int a, int b) return null; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetStatus(this BlockBodiedMethodTests.Entity entity) { if (entity.IsActive) @@ -428,7 +428,7 @@ public static string GetStatus(this BlockBodiedMethodTests.Entity entity) return "Inactive"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetValueLabel(this BlockBodiedMethodTests.Entity entity) { switch (entity.Value) @@ -444,7 +444,7 @@ public static string GetValueLabel(this BlockBodiedMethodTests.Entity entity) } } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetPriority(this BlockBodiedMethodTests.Entity entity) { switch (entity.Value) @@ -465,7 +465,7 @@ public static string GetPriority(this BlockBodiedMethodTests.Entity entity) } } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetValueCategory(this BlockBodiedMethodTests.Entity entity) { if (entity.Value > 100) @@ -486,19 +486,19 @@ public static string GetValueCategory(this BlockBodiedMethodTests.Entity entity) return "Low"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetNameOrDefault(this BlockBodiedMethodTests.Entity entity) { return entity.Name ?? "Unknown"; } - [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)] + [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite, AllowBlockBody = true)] public static int? GetNameLength(this BlockBodiedMethodTests.Entity entity) { return entity.Name?.Length; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetValueLabelModern(this BlockBodiedMethodTests.Entity entity) { return entity.Value switch @@ -510,7 +510,7 @@ public static string GetValueLabelModern(this BlockBodiedMethodTests.Entity enti }; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetPriorityModern(this BlockBodiedMethodTests.Entity entity) { return entity.Value switch @@ -522,7 +522,7 @@ public static string GetPriorityModern(this BlockBodiedMethodTests.Entity entity }; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int CalculateComplex(this BlockBodiedMethodTests.Entity entity) { var doubled = entity.Value * 2; @@ -531,7 +531,7 @@ public static int CalculateComplex(this BlockBodiedMethodTests.Entity entity) return sum + 10; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetComplexCategory(this BlockBodiedMethodTests.Entity entity) { if (entity.IsActive && entity.Value > 100) @@ -552,7 +552,7 @@ public static string GetComplexCategory(this BlockBodiedMethodTests.Entity entit return "Other"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetGuardedValue(this BlockBodiedMethodTests.Entity entity) { if (!entity.IsActive) @@ -568,7 +568,7 @@ public static int GetGuardedValue(this BlockBodiedMethodTests.Entity entity) return entity.Value * 2; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetCombinedLogic(this BlockBodiedMethodTests.Entity entity) { if (entity.IsActive) @@ -587,19 +587,19 @@ public static string GetCombinedLogic(this BlockBodiedMethodTests.Entity entity) return "Inactive"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetValueUsingTernary(this BlockBodiedMethodTests.Entity entity) { return entity.IsActive ? "Active" : "Inactive"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetNestedTernary(this BlockBodiedMethodTests.Entity entity) { return entity.Value > 100 ? "High" : entity.Value > 50 ? "Medium" : "Low"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetComplexMix(this BlockBodiedMethodTests.Entity entity) { if (entity.IsActive) @@ -615,7 +615,7 @@ public static string GetComplexMix(this BlockBodiedMethodTests.Entity entity) return "Inactive"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetValueWithCondition(this BlockBodiedMethodTests.Entity entity) { return entity.Value switch @@ -627,14 +627,14 @@ public static string GetValueWithCondition(this BlockBodiedMethodTests.Entity en }; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int CalculateWithReuse(this BlockBodiedMethodTests.Entity entity) { var doubled = entity.Value * 2; return doubled + doubled; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static bool IsHighValue(this BlockBodiedMethodTests.Entity entity) { if (entity.Value > 100) @@ -644,7 +644,7 @@ public static bool IsHighValue(this BlockBodiedMethodTests.Entity entity) return false; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetInactiveStatus(this BlockBodiedMethodTests.Entity entity) { if (!entity.IsActive) @@ -657,13 +657,13 @@ public static string GetInactiveStatus(this BlockBodiedMethodTests.Entity entity } } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetFormattedValue(this BlockBodiedMethodTests.Entity entity) { return $"Value: {entity.Value}"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static double CalculatePercentage(this BlockBodiedMethodTests.Entity entity) { return (double)entity.Value / 100.0 * 50.0; diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs index c09237b..99cbb3e 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTest.cs @@ -124,19 +124,19 @@ public Task BlockBodyCallingProjectableMethod_InLogicalExpression() public static class ProjectableCallExtensions { // Base projectable methods (helper methods) - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetConstant(this BlockBodyProjectableCallTests.Entity entity) { return 42; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetDoubled(this BlockBodyProjectableCallTests.Entity entity) { return entity.Value * 2; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetCategory(this BlockBodyProjectableCallTests.Entity entity) { if (entity.Value > 100) @@ -145,7 +145,7 @@ public static string GetCategory(this BlockBodyProjectableCallTests.Entity entit return "Low"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetLevel(this BlockBodyProjectableCallTests.Entity entity) { if (entity.Value > 100) return "Level3"; @@ -153,7 +153,7 @@ public static string GetLevel(this BlockBodyProjectableCallTests.Entity entity) return "Level1"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static bool IsHighValue(this BlockBodyProjectableCallTests.Entity entity) { return entity.Value > 100; @@ -161,20 +161,20 @@ public static bool IsHighValue(this BlockBodyProjectableCallTests.Entity entity) // Block-bodied methods calling projectable methods - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetAdjustedWithConstant(this BlockBodyProjectableCallTests.Entity entity) { return entity.Value + entity.GetConstant(); } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetDoubledValue(this BlockBodyProjectableCallTests.Entity entity) { var doubled = entity.GetDoubled(); return doubled; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetCategoryBasedOnAdjusted(this BlockBodyProjectableCallTests.Entity entity) { if (entity.GetDoubled() > 200) @@ -187,13 +187,13 @@ public static string GetCategoryBasedOnAdjusted(this BlockBodyProjectableCallTes } } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int CombineProjectableMethods(this BlockBodyProjectableCallTests.Entity entity) { return entity.GetDoubled() + entity.GetConstant(); } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetLabelBasedOnCategory(this BlockBodyProjectableCallTests.Entity entity) { switch (entity.GetCategory()) @@ -207,7 +207,7 @@ public static string GetLabelBasedOnCategory(this BlockBodyProjectableCallTests. } } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetDescriptionByLevel(this BlockBodyProjectableCallTests.Entity entity) { return entity.GetLevel() switch @@ -219,7 +219,7 @@ public static string GetDescriptionByLevel(this BlockBodyProjectableCallTests.En }; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int CalculateUsingProjectable(this BlockBodyProjectableCallTests.Entity entity) { var doubled = entity.GetDoubled(); @@ -227,13 +227,13 @@ public static int CalculateUsingProjectable(this BlockBodyProjectableCallTests.E return withConstant * 2; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static int GetNestedProjectableCall(this BlockBodyProjectableCallTests.Entity entity) { return entity.GetAdjustedWithConstant() + 10; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetStatusWithProjectableCheck(this BlockBodyProjectableCallTests.Entity entity) { if (entity.IsHighValue()) @@ -245,13 +245,13 @@ public static string GetStatusWithProjectableCheck(this BlockBodyProjectableCall return "Normal"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetConditionalProjectable(this BlockBodyProjectableCallTests.Entity entity) { return entity.IsActive ? entity.GetCategory() : "Inactive"; } - [Projectable] + [Projectable(AllowBlockBody = true)] public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity entity) { var doubled = entity.GetDoubled(); @@ -264,7 +264,7 @@ public static string GetChainedResult(this BlockBodyProjectableCallTests.Entity return entity.GetLevel(); } - [Projectable] + [Projectable(AllowBlockBody = true)] public static bool IsComplexCondition(this BlockBodyProjectableCallTests.Entity entity) { return entity.IsActive && entity.IsHighValue() || entity.GetDoubled() > 150; diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.verified.txt new file mode 100644 index 0000000..1614e52 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitExpressionGetter.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar + 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.verified.txt new file mode 100644 index 0000000..c9f2bbb --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetter.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.verified.txt new file mode 100644 index 0000000..c9f2bbb --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitExpressionGetter.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => 1; + } + } +} \ 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 ae09af6..b459e43 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -201,6 +201,179 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public Task ProjectablePropertyWithExplicitExpressionGetter() + { + // Tests explicit getter with expression body: { get => expression; } + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + [Projectable] + public int Foo { get => 1; } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectablePropertyWithExplicitBlockGetter() + { + // Tests explicit getter with block body: { get { return expression; } } + // Requires AllowBlockBody = true + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + [Projectable(AllowBlockBody = true)] + public int Foo { get { return 1; } } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectableComputedPropertyWithExplicitExpressionGetter() + { + // Tests explicit getter with expression body accessing other properties + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable] + public int Foo { get => Bar + 1; } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + +// [Fact] +// public Task ProjectableComputedPropertyWithExplicitBlockGetter() +// { +// // Tests explicit getter with block body accessing other properties +// // Requires AllowBlockBody = true +// var compilation = CreateCompilation(@" +// using System; +// using EntityFrameworkCore.Projectables; +// namespace Foo { +// class C { +// public int Bar { get; set; } +// +// [Projectable(AllowBlockBody = true)] +// public int Foo { get { return Bar + 1; } } +// } +// } +// "); +// +// var result = RunGenerator(compilation); +// +// Assert.Empty(result.Diagnostics); +// Assert.Single(result.GeneratedTrees); +// +// return Verifier.Verify(result.GeneratedTrees[0].ToString()); +// } + +// [Fact] +// public Task ProjectablePropertyWithExplicitBlockGetterUsingThis() +// { +// // Tests explicit getter with block body using 'this' qualifier +// // Requires AllowBlockBody = true +// var compilation = CreateCompilation(@" +// using System; +// using EntityFrameworkCore.Projectables; +// namespace Foo { +// class C { +// public int Bar { get; set; } +// +// [Projectable(AllowBlockBody = true)] +// public int Foo { get { return this.Bar; } } +// } +// } +// "); +// +// var result = RunGenerator(compilation); +// +// Assert.Empty(result.Diagnostics); +// Assert.Single(result.GeneratedTrees); +// +// return Verifier.Verify(result.GeneratedTrees[0].ToString()); +// } + +// [Fact] +// public Task ProjectablePropertyWithExplicitBlockGetterAndMethodCall() +// { +// // Tests explicit getter with block body calling other methods +// // Requires AllowBlockBody = true +// var compilation = CreateCompilation(@" +// using System; +// using EntityFrameworkCore.Projectables; +// namespace Foo { +// class C { +// public int Bar() => 1; +// +// [Projectable(AllowBlockBody = true)] +// public int Foo { get { return Bar(); } } +// } +// } +// "); +// +// var result = RunGenerator(compilation); +// +// Assert.Empty(result.Diagnostics); +// Assert.Single(result.GeneratedTrees); +// +// return Verifier.Verify(result.GeneratedTrees[0].ToString()); +// } + + [Fact] + public void ProjectablePropertyWithExplicitBlockGetter_WithoutAllowBlockBody_EmitsWarning() + { + // Tests that block-bodied property getter without AllowBlockBody = true emits a warning + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + [Projectable] + public int Foo { get { return 1; } } + } +} +"); + + var result = RunGenerator(compilation); + + // Should have a warning about experimental feature + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("EFP0001", diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + } + [Fact] public Task MoreComplexProjectableComputedProperty() @@ -470,28 +643,6 @@ static class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } - [Fact] - public void BlockBodiedMember_RaisesDiagnostics() - { - var compilation = CreateCompilation(@" -using System; -using EntityFrameworkCore.Projectables; -namespace Foo { - class C { - [Projectable] - public int Foo - { - get => 1; - } - } -} -"); - - var result = RunGenerator(compilation); - - Assert.Single(result.Diagnostics); - } - [Fact] public void BlockBodiedMethod_NoLongerRaisesDiagnostics() { @@ -500,7 +651,7 @@ public void BlockBodiedMethod_NoLongerRaisesDiagnostics() using EntityFrameworkCore.Projectables; namespace Foo { class C { - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { return 1; @@ -1987,7 +2138,7 @@ public Task BlockBodiedMethod_SimpleReturn() using EntityFrameworkCore.Projectables; namespace Foo { class C { - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { return 42; @@ -2014,7 +2165,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { return Bar + 10; @@ -2041,7 +2192,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { if (Bar > 10) @@ -2075,7 +2226,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public string Foo() { if (Bar > 10) @@ -2113,7 +2264,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { var temp = Bar * 2; @@ -2141,7 +2292,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { var a = Bar * 2; @@ -2170,7 +2321,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { var threshold = Bar * 2; @@ -2205,7 +2356,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public string Foo() { var value = Bar * 2; @@ -2241,7 +2392,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { if (Bar > 10) @@ -2272,7 +2423,7 @@ public Task BlockBodiedMethod_WithMultipleParameters() using EntityFrameworkCore.Projectables; namespace Foo { class C { - [Projectable] + [Projectable(AllowBlockBody = true)] public int Add(int a, int b) { return a + b; @@ -2300,7 +2451,7 @@ class C { public int Bar { get; set; } public bool IsActive { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { if (IsActive && Bar > 0) @@ -2335,7 +2486,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { if (Bar > 10) @@ -2366,7 +2517,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int? Foo() { if (Bar > 10) @@ -2396,7 +2547,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public string Foo() { switch (Bar) @@ -2431,7 +2582,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public string Foo() { switch (Bar) @@ -2469,7 +2620,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public string? Foo() { switch (Bar) @@ -2502,7 +2653,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { Bar = 10; @@ -2531,7 +2682,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { Bar += 10; @@ -2560,7 +2711,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { var x = 5; @@ -2590,7 +2741,7 @@ namespace Foo { class C { public int Bar { get; set; } - [Projectable] + [Projectable(AllowBlockBody = true)] public int Foo() { Console.WriteLine(""test""); @@ -3170,6 +3321,58 @@ public record Entity return Verifier.Verify(result.GeneratedTrees[0].ToString()); } + [Fact] + public void BlockBodiedMethod_WithoutAllowFlag_EmitsWarning() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class C { + public int Value { get; set; } + + [Projectable] + public int GetDouble() + { + return Value * 2; + } + } +} +"); + var result = RunGenerator(compilation); + + // Should have a warning about experimental feature + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal("EFP0001", diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); + } + + [Fact] + public void BlockBodiedMethod_WithAllowFlag_NoWarning() + { + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; + +namespace Foo { + class C { + public int Value { get; set; } + + [Projectable(AllowBlockBody = true)] + public int GetDouble() + { + return Value * 2; + } + } +} +"); + var result = RunGenerator(compilation); + + // Should have no warnings + Assert.Empty(result.Diagnostics); + } + #region Helpers Compilation CreateCompilation(string source, bool expectedToCompile = true) From 3788729afd29dec42d6e978b0ffd1eaa6548a300 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 18 Feb 2026 12:58:35 +0100 Subject: [PATCH 24/26] Fix block bodied properties --- .../ProjectableInterpreter.cs | 17 +- ...opertyWithExplicitBlockGetter.verified.txt | 17 ++ ...licitBlockGetterAndMethodCall.verified.txt | 17 ++ ...hExplicitBlockGetterUsingThis.verified.txt | 17 ++ .../ProjectionExpressionGeneratorTests.cs | 154 +++++++++--------- 5 files changed, 143 insertions(+), 79 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs index 356420b..3037fb1 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableInterpreter.cs @@ -394,11 +394,11 @@ x is IPropertySymbol xProperty && else if (memberBody is PropertyDeclarationSyntax propertyDeclarationSyntax) { ExpressionSyntax? bodyExpression = null; + var isBlockBodiedGetter = false; // Expression-bodied property: int Prop => value; if (propertyDeclarationSyntax.ExpressionBody is not null) { - bodyExpression = propertyDeclarationSyntax.ExpressionBody.Expression; } else if (propertyDeclarationSyntax.AccessorList is not null) @@ -424,6 +424,15 @@ x is IPropertySymbol xProperty && var blockConverter = new BlockStatementConverter(context, expressionSyntaxRewriter); bodyExpression = blockConverter.TryConvertBlock(getter.Body, memberSymbol.Name); + isBlockBodiedGetter = true; + + if (bodyExpression is null) + { + // Diagnostics already reported by BlockStatementConverter + return null; + } + + // The expression has already been rewritten by BlockStatementConverter, so we don't rewrite it again } } @@ -437,7 +446,11 @@ x is IPropertySymbol xProperty && var returnType = declarationSyntaxRewriter.Visit(propertyDeclarationSyntax.Type); descriptor.ReturnTypeName = returnType.ToString(); - descriptor.ExpressionBody = (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression); + + // Only rewrite expression-bodied properties, block-bodied getters are already rewritten + descriptor.ExpressionBody = isBlockBodiedGetter + ? bodyExpression + : (ExpressionSyntax)expressionSyntaxRewriter.Visit(bodyExpression); } else { diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.verified.txt new file mode 100644 index 0000000..1614e52 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectableComputedPropertyWithExplicitBlockGetter.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar + 1; + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.verified.txt new file mode 100644 index 0000000..fb4be05 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterAndMethodCall.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.Bar(); + } + } +} \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.verified.txt new file mode 100644 index 0000000..3ad21d6 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.ProjectablePropertyWithExplicitBlockGetterUsingThis.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_C_Foo + { + static global::System.Linq.Expressions.Expression> Expression() + { + return (global::Foo.C @this) => @this.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 b459e43..ea03112 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.cs @@ -273,83 +273,83 @@ class C { return Verifier.Verify(result.GeneratedTrees[0].ToString()); } -// [Fact] -// public Task ProjectableComputedPropertyWithExplicitBlockGetter() -// { -// // Tests explicit getter with block body accessing other properties -// // Requires AllowBlockBody = true -// var compilation = CreateCompilation(@" -// using System; -// using EntityFrameworkCore.Projectables; -// namespace Foo { -// class C { -// public int Bar { get; set; } -// -// [Projectable(AllowBlockBody = true)] -// public int Foo { get { return Bar + 1; } } -// } -// } -// "); -// -// var result = RunGenerator(compilation); -// -// Assert.Empty(result.Diagnostics); -// Assert.Single(result.GeneratedTrees); -// -// return Verifier.Verify(result.GeneratedTrees[0].ToString()); -// } - -// [Fact] -// public Task ProjectablePropertyWithExplicitBlockGetterUsingThis() -// { -// // Tests explicit getter with block body using 'this' qualifier -// // Requires AllowBlockBody = true -// var compilation = CreateCompilation(@" -// using System; -// using EntityFrameworkCore.Projectables; -// namespace Foo { -// class C { -// public int Bar { get; set; } -// -// [Projectable(AllowBlockBody = true)] -// public int Foo { get { return this.Bar; } } -// } -// } -// "); -// -// var result = RunGenerator(compilation); -// -// Assert.Empty(result.Diagnostics); -// Assert.Single(result.GeneratedTrees); -// -// return Verifier.Verify(result.GeneratedTrees[0].ToString()); -// } - -// [Fact] -// public Task ProjectablePropertyWithExplicitBlockGetterAndMethodCall() -// { -// // Tests explicit getter with block body calling other methods -// // Requires AllowBlockBody = true -// var compilation = CreateCompilation(@" -// using System; -// using EntityFrameworkCore.Projectables; -// namespace Foo { -// class C { -// public int Bar() => 1; -// -// [Projectable(AllowBlockBody = true)] -// public int Foo { get { return Bar(); } } -// } -// } -// "); -// -// var result = RunGenerator(compilation); -// -// Assert.Empty(result.Diagnostics); -// Assert.Single(result.GeneratedTrees); -// -// return Verifier.Verify(result.GeneratedTrees[0].ToString()); -// } + [Fact] + public Task ProjectableComputedPropertyWithExplicitBlockGetter() + { + // Tests explicit getter with block body accessing other properties + // Requires AllowBlockBody = true + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable(AllowBlockBody = true)] + public int Foo { get { return Bar + 1; } } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectablePropertyWithExplicitBlockGetterUsingThis() + { + // Tests explicit getter with block body using 'this' qualifier + // Requires AllowBlockBody = true + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar { get; set; } + + [Projectable(AllowBlockBody = true)] + public int Foo { get { return this.Bar; } } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } + + [Fact] + public Task ProjectablePropertyWithExplicitBlockGetterAndMethodCall() + { + // Tests explicit getter with block body calling other methods + // Requires AllowBlockBody = true + var compilation = CreateCompilation(@" +using System; +using EntityFrameworkCore.Projectables; +namespace Foo { + class C { + public int Bar() => 1; + + [Projectable(AllowBlockBody = true)] + public int Foo { get { return Bar(); } } + } +} +"); + + var result = RunGenerator(compilation); + + Assert.Empty(result.Diagnostics); + Assert.Single(result.GeneratedTrees); + + return Verifier.Verify(result.GeneratedTrees[0].ToString()); + } [Fact] public void ProjectablePropertyWithExplicitBlockGetter_WithoutAllowBlockBody_EmitsWarning() From f2d0ec8974e5978ae3fdb37eff9b4a539e183853 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 18 Feb 2026 13:37:34 +0100 Subject: [PATCH 25/26] Simplify code and add xmldocs --- .../BlockStatementConverter.cs | 153 ++++++++++++------ 1 file changed, 101 insertions(+), 52 deletions(-) diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index 55abd92..41a5b4c 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -44,6 +44,9 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax return TryConvertStatements(block.Statements.ToList(), memberName); } + /// + /// Tries to convert a list of statements into a single expression. This is used for the body of the method or property. + /// private ExpressionSyntax? TryConvertStatements(List statements, string memberName) { if (statements.Count == 0) @@ -146,6 +149,9 @@ public BlockStatementConverter(SourceProductionContext context, ExpressionSyntax return TryConvertStatement(lastStatement, memberName); } + /// + /// Processes a local variable declaration statement, rewriting the initializer and storing it in the local variables dictionary. + /// private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDecl, string memberName) { foreach (var variable in localDecl.Declaration.Variables) @@ -171,6 +177,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec return true; } + /// + /// Tries to convert a single statement into an expression. This is used for return statements, if statements, and switch statements. + /// private ExpressionSyntax? TryConvertStatement(StatementSyntax statement, string memberName) { switch (statement) @@ -213,6 +222,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec } } + /// + /// Converts a return statement to its expression, after rewriting it and replacing any local variable references. + /// private ExpressionSyntax? TryConvertReturnStatement(ReturnStatementSyntax returnStmt, string memberName) { if (returnStmt.Expression == null) @@ -230,6 +242,9 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec return expression; } + /// + /// Converts an if statement (with optional else) to a conditional expression. + /// private ConditionalExpressionSyntax? TryConvertIfStatement(IfStatementSyntax ifStmt, string memberName) { // Convert if-else to conditional (ternary) expression @@ -272,12 +287,16 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec ); } + /// + /// Converts a switch statement to nested conditional expressions. + /// private ExpressionSyntax? TryConvertSwitchStatement(SwitchStatementSyntax switchStmt, string memberName) { // Convert switch statement to nested conditional expressions // Process sections in reverse order to build from the default case up - var switchExpression = (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression); + var switchExpression = + (ExpressionSyntax)_expressionRewriter.Visit(switchStmt.Expression); // Replace any local variable references in the switch expression switchExpression = ReplaceLocalVariables(switchExpression); @@ -374,10 +393,12 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec return currentExpression; } + /// + /// Converts a switch section to an expression. This assumes the section has already been validated to only contain supported statements. + /// private ExpressionSyntax? ConvertSwitchSection(SwitchSectionSyntax section, string memberName) { // Convert the statements in the switch section - // Most switch sections end with break, return, or throw var statements = section.Statements.ToList(); // Remove trailing break statements as they're not needed in expressions @@ -386,21 +407,18 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec statements = statements.Take(statements.Count - 1).ToList(); } - if (statements.Count != 0) + if (statements.Count > 0) { return TryConvertStatements(statements, memberName); } - // Use the section's first label location for error reporting + // Empty section - report diagnostic var firstLabel = section.Labels.FirstOrDefault(); - if (firstLabel == null) - { - return null; - } - + var location = firstLabel?.GetLocation() ?? section.GetLocation(); + var diagnostic = Diagnostic.Create( Diagnostics.UnsupportedStatementInBlockBody, - firstLabel.GetLocation(), + location, memberName, "Switch section must have at least one statement" ); @@ -409,60 +427,94 @@ private bool TryProcessLocalDeclaration(LocalDeclarationStatementSyntax localDec } + /// + /// Replaces references to local variables in the given expression with their initializer expressions. + /// private ExpressionSyntax ReplaceLocalVariables(ExpressionSyntax expression) { // Use a rewriter to replace local variable references with their initializer expressions var rewriter = new LocalVariableReplacer(_localVariables); return (ExpressionSyntax)rewriter.Visit(expression); } - + + /// + /// Analyzes an expression statement for side effects. If it has side effects, reports a diagnostic and returns null. + /// private ExpressionSyntax? AnalyzeExpressionStatement(ExpressionStatementSyntax exprStmt, string memberName) { var expression = exprStmt.Expression; - // Check for specific side effects - switch (expression) + // Check for specific side effects that are always errors + if (HasSideEffects(expression, out var errorMessage)) { - case AssignmentExpressionSyntax assignment: - ReportSideEffect(assignment, GetAssignmentErrorMessage(assignment)); + ReportSideEffect(expression, errorMessage); + return null; + } + + // Check for potentially impure method calls + if (expression is InvocationExpressionSyntax invocation) + { + if (!IsProjectableMethodCall(invocation, out var warningMessage)) + { + ReportPotentialSideEffect(invocation, warningMessage); return null; - - case PostfixUnaryExpressionSyntax postfix when + } + } + + // Expression statements without side effects are still not supported in the current design + ReportUnsupportedStatement(exprStmt, memberName, + "Expression statements are not supported in projectable methods. Consider removing this statement or converting it to a return statement."); + return null; + } + + /// + /// Checks if an expression has side effects. + /// + private bool HasSideEffects(ExpressionSyntax expression, out string errorMessage) + { + return expression switch + { + AssignmentExpressionSyntax assignment => (errorMessage = GetAssignmentErrorMessage(assignment)) != null, + + PostfixUnaryExpressionSyntax postfix when postfix.IsKind(SyntaxKind.PostIncrementExpression) || - postfix.IsKind(SyntaxKind.PostDecrementExpression): - ReportSideEffect(postfix, $"Increment/decrement operator '{postfix.OperatorToken.Text}' has side effects and cannot be used in projectable methods"); - return null; - - case PrefixUnaryExpressionSyntax prefix when + postfix.IsKind(SyntaxKind.PostDecrementExpression) + => (errorMessage = $"Increment/decrement operator '{postfix.OperatorToken.Text}' has side effects and cannot be used in projectable methods") != null, + + PrefixUnaryExpressionSyntax prefix when prefix.IsKind(SyntaxKind.PreIncrementExpression) || - prefix.IsKind(SyntaxKind.PreDecrementExpression): - ReportSideEffect(prefix, $"Increment/decrement operator '{prefix.OperatorToken.Text}' has side effects and cannot be used in projectable methods"); - return null; + prefix.IsKind(SyntaxKind.PreDecrementExpression) + => (errorMessage = $"Increment/decrement operator '{prefix.OperatorToken.Text}' has side effects and cannot be used in projectable methods") != null, + + _ => (errorMessage = string.Empty) == null + }; + } + + /// + /// Checks if a method invocation is to a projectable method. + /// + private bool IsProjectableMethodCall(InvocationExpressionSyntax invocation, out string warningMessage) + { + var symbolInfo = _expressionRewriter.GetSemanticModel().GetSymbolInfo(invocation); + if (symbolInfo.Symbol is IMethodSymbol methodSymbol) + { + var hasProjectableAttr = methodSymbol.GetAttributes() + .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute"); - case InvocationExpressionSyntax invocation: - // Check if this is a potentially impure method call - var symbolInfo = _expressionRewriter.GetSemanticModel().GetSymbolInfo(invocation); - if (symbolInfo.Symbol is IMethodSymbol methodSymbol) - { - // Check if method has [Projectable] attribute - those are safe - var hasProjectableAttr = methodSymbol.GetAttributes() - .Any(attr => attr.AttributeClass?.Name == "ProjectableAttribute"); - - if (!hasProjectableAttr) - { - ReportPotentialSideEffect(invocation, - $"Method call '{methodSymbol.Name}' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods"); - return null; - } - } - break; + if (!hasProjectableAttr) + { + warningMessage = $"Method call '{methodSymbol.Name}' may have side effects. Only calls to methods marked with [Projectable] are guaranteed to be safe in projectable methods"; + return false; + } } - // If we got here, it's an expression statement we don't support - ReportUnsupportedStatement(exprStmt, memberName, "Expression statements are not supported in projectable methods"); - return null; + warningMessage = string.Empty; + return true; } + /// + /// Generates an error message for an assignment expression, indicating that it has side effects and cannot be used in projectable methods. + /// private string GetAssignmentErrorMessage(AssignmentExpressionSyntax assignment) { var operatorText = assignment.OperatorToken.Text; @@ -473,13 +525,11 @@ private string GetAssignmentErrorMessage(AssignmentExpressionSyntax assignment) { return $"Property assignment '{memberAccess.Name}' has side effects and cannot be used in projectable methods"; } - return $"Assignment operation has side effects and cannot be used in projectable methods"; - } - else - { - // Compound assignment like +=, -=, etc. - return $"Compound assignment operator '{operatorText}' has side effects and cannot be used in projectable methods"; + return "Assignment operation has side effects and cannot be used in projectable methods"; } + + // Compound assignment like +=, -=, etc. + return $"Compound assignment operator '{operatorText}' has side effects and cannot be used in projectable methods"; } private void ReportSideEffect(SyntaxNode node, string message) @@ -513,7 +563,6 @@ private void ReportUnsupportedStatement(StatementSyntax statement, string member _context.ReportDiagnostic(diagnostic); } - private class LocalVariableReplacer : CSharpSyntaxRewriter { private readonly Dictionary _localVariables; From 9b7e61a9a760d108eb2738b845c27324b075238c Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 18 Feb 2026 15:05:04 +0100 Subject: [PATCH 26/26] Handle code review suggestions and fix an operator precedence issue with parenthesis --- .../AnalyzerReleases.Shipped.md | 12 ++++++------ .../BlockStatementConverter.cs | 6 ++++-- ...eMethod_WithLocalVariable.DotNet10_0.verified.txt | 2 +- ...leMethod_WithLocalVariable.DotNet9_0.verified.txt | 2 +- ...gProjectableMethod_WithLocalVariable.verified.txt | 2 +- ...BlockBodiedMethod_LocalInIfCondition.verified.txt | 2 +- ...BodiedMethod_LocalInSwitchExpression.verified.txt | 2 +- ....BlockBodiedMethod_WithLocalVariable.verified.txt | 2 +- ...dMethod_WithTransitiveLocalVariables.verified.txt | 2 +- 9 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md index 7bd9067..253db78 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md +++ b/src/EntityFrameworkCore.Projectables.Generator/AnalyzerReleases.Shipped.md @@ -3,15 +3,15 @@ ### New Rules Rule ID | Category | Severity | Notes ---------|----------|----------|--------------------------------------------------------------------------- -EFP0003 | Design | Warning | Unsupported statement in block-bodied method -EFP0004 | Design | Error | Statement with side effects in block-bodied method -EFP0005 | Design | Warning | Potential side effect in block-bodied method -EFP0006 | Design | Error | Method or property should expose an body definition (block or expression) +--------|----------|----------|------------------------------------------------------------------------- +EFP0003 | Design | Warning | Unsupported statement in block-bodied method +EFP0004 | Design | Error | Statement with side effects in block-bodied method +EFP0005 | Design | Warning | Potential side effect in block-bodied method +EFP0006 | Design | Error | Method or property should expose a body definition (block or expression) ### Changed Rules -Rule ID | New Category | New Severity | Old Category | Old Severity | Notes"; +Rule ID | New Category | New Severity | Old Category | Old Severity | Notes --------|--------------|--------------|--------------|--------------|----------------------------------------------------------------- EFP0001 | Design | Warning | Design | Error | Changed to warning for experimental block-bodied members support diff --git a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs index 41a5b4c..16f7fae 100644 --- a/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs +++ b/src/EntityFrameworkCore.Projectables.Generator/BlockStatementConverter.cs @@ -577,8 +577,10 @@ public LocalVariableReplacer(Dictionary localVariables var identifier = node.Identifier.Text; if (_localVariables.TryGetValue(identifier, out var replacement)) { - // Replace the identifier with the expression it was initialized with - return replacement.WithTriviaFrom(node); + // Replace the identifier with the expression it was initialized with, + // wrapping in parentheses to preserve operator precedence. + var parenthesized = SyntaxFactory.ParenthesizedExpression(replacement.WithoutTrivia()); + return parenthesized.WithTriviaFrom(node); } return base.VisitIdentifierName(node); diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt index 0294ea7..ae5ad93 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet10_0.verified.txt @@ -1,2 +1,2 @@ -SELECT [e].[Value] * 2 + 84 +SELECT ([e].[Value] * 2 + 42) * 2 FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt index 0294ea7..ae5ad93 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.DotNet9_0.verified.txt @@ -1,2 +1,2 @@ -SELECT [e].[Value] * 2 + 84 +SELECT ([e].[Value] * 2 + 42) * 2 FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt index 0294ea7..ae5ad93 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/BlockBodiedMethods/BlockBodyProjectableCallTests.BlockBodyCallingProjectableMethod_WithLocalVariable.verified.txt @@ -1,2 +1,2 @@ -SELECT [e].[Value] * 2 + 84 +SELECT ([e].[Value] * 2 + 42) * 2 FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt index e940c26..47b44c4 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInIfCondition.verified.txt @@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated { static global::System.Linq.Expressions.Expression> Expression() { - return (global::Foo.C @this) => @this.Bar * 2 > 10 ? 1 : 0; + return (global::Foo.C @this) => (@this.Bar * 2) > 10 ? 1 : 0; } } } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt index 0a7e7da..ce11b5b 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_LocalInSwitchExpression.verified.txt @@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated { static global::System.Linq.Expressions.Expression> Expression() { - return (global::Foo.C @this) => @this.Bar * 2 == 2 ? "Two" : @this.Bar * 2 == 4 ? "Four" : "Other"; + return (global::Foo.C @this) => (@this.Bar * 2) == 2 ? "Two" : (@this.Bar * 2) == 4 ? "Four" : "Other"; } } } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt index d863659..44c2e0f 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithLocalVariable.verified.txt @@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated { static global::System.Linq.Expressions.Expression> Expression() { - return (global::Foo.C @this) => @this.Bar * 2 + 5; + return (global::Foo.C @this) => (@this.Bar * 2) + 5; } } } \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt index 24ae821..3e8b98c 100644 --- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTests.BlockBodiedMethod_WithTransitiveLocalVariables.verified.txt @@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Generated { static global::System.Linq.Expressions.Expression> Expression() { - return (global::Foo.C @this) => @this.Bar * 2 + 5 + 10; + return (global::Foo.C @this) => ((@this.Bar * 2) + 5) + 10; } } } \ No newline at end of file