using ExtractMethod.Tooling; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace BeforeAfter.Tests.ExtractMethod; /// /// Tests for yak em 04 (codegen): generate the refactoring from the em 03 /// suggestion — new method (promoted signature, parameter names from the /// source symbols) + rewritten call site + transformed tree — and the /// round-trip check (the transformed tree compiles with zero diagnostics). /// /// Fixture line anchors: the em 01/02/03 anchors (ScoreReads 56..58, /// Summarize 68..76 / partial 68..72, Heaviest 87..93) plus the em 04 /// additions Accumulate for-loop 127..130 and Pairwise 141..147. /// public class RefactoringTests { // --------------------------------------------------------------------- // Summarize partial (68..72): the declared-inside `total` promotes to the // plain return of the new method; at the call site it is re-declared from // the result. The new body gains the appended `return total;`. // --------------------------------------------------------------------- [Fact] public void Summarize_partial_selection_generates_returning_method_and_declaration_call() { var (method, calls, tree) = Generate(68, 72); Assert.Equal("Extract", method.Identifier.ValueText); Assert.Equal("int", method.ReturnType.ToString()); Assert.Equal(new[] { "int limit" }, method.ParameterList.Parameters.Select(p => p.ToString())); var body = (BlockSyntax)method.Body!; Assert.Equal(3, body.Statements.Count); // total declaration, for-loop, appended return Assert.Equal("return total;", body.Statements[2].ToString()); // `total` was declared inside the selection: re-declared at the call // site from the result. Assert.Single(calls); Assert.Equal("int total = Extract(limit);", calls[0].ToString()); VerifyZeroDiagnostics(tree); } // --------------------------------------------------------------------- // Summarize full body (68..76): the selection ends the (non-void) // enclosing method and the trailing `return message;` is nameable — the // call site becomes `return Extract(limit);` and NO extra return is // appended to the new body (the existing one is kept). // --------------------------------------------------------------------- [Fact] public void Summarize_full_body_selection_becomes_return_call() { var (method, calls, tree) = Generate(68, 76); Assert.Equal("Extract", method.Identifier.ValueText); Assert.Equal("string", method.ReturnType.ToString()); Assert.Equal(new[] { "int limit" }, method.ParameterList.Parameters.Select(p => p.ToString())); var body = (BlockSyntax)method.Body!; Assert.Equal(5, body.Statements.Count); // total, for, scaled, message, return — unchanged Assert.Equal("return message;", body.Statements[^1].ToString()); Assert.Single(calls); Assert.Equal("return Extract(limit);", calls[0].ToString()); VerifyZeroDiagnostics(tree); } // --------------------------------------------------------------------- // Heaviest full body (87..93): the trailing `return best;` names the // single return slot, which matches the enclosing method's return type — // again a `return Extract(...)` call site, with the widgets/count // parameters as plain in-params. // --------------------------------------------------------------------- [Fact] public void Heaviest_full_body_selection_becomes_return_call() { var (method, calls, tree) = Generate(87, 93); Assert.Equal("Widget", method.ReturnType.ToString()); Assert.Equal(new[] { "int count", "List widgets" }, method.ParameterList.Parameters.Select(p => p.ToString())); var body = (BlockSyntax)method.Body!; Assert.Equal(3, body.Statements.Count); Assert.Equal("return best;", body.Statements[^1].ToString()); Assert.Single(calls); Assert.Equal("return Extract(count, widgets);", calls[0].ToString()); VerifyZeroDiagnostics(tree); } // --------------------------------------------------------------------- // ScoreReads (56..58): the enclosing method returns int but the selection // ends with the COMPOSITE `return score + bonus;` — not nameable in v1, // so the promoted signature is void and codegen must REFUSE (the report // note already says: extract a local first, then re-run). // --------------------------------------------------------------------- [Fact] public void ScoreReads_refuses_when_the_composite_trailing_return_is_not_nameable() { var (tree, compilation) = DemoFixture.Load(); var model = compilation.GetSemanticModel(tree); var resolved = SelectionResolver.Resolve(tree, 56, 58); Assert.True(resolved.Succeeded, resolved.Error); var suggestion = DataFlowClassifier.Classify(model, resolved); var signature = SignatureBuilder.Build(model, resolved, suggestion); Assert.Equal("void", signature.ReturnType); var ex = Assert.Throws( () => RefactoringGenerator.Generate(model, resolved, signature)); Assert.Contains("extract it into a local first", ex.Message, StringComparison.Ordinal); } // --------------------------------------------------------------------- // Accumulate for-loop (127..130): `acc` is declared BEFORE the selection, // written inside and read after — the promoted signature keeps it as a // ref parameter (the write-back flows the value out; void return, no // return slot) and the call site is a plain call with `ref acc`. // --------------------------------------------------------------------- [Fact] public void Accumulate_forloop_selection_calls_with_ref_write_back() { var (method, calls, tree) = Generate(127, 130); Assert.Equal("void", method.ReturnType.ToString()); Assert.Equal(new[] { "ref int acc", "int n" }, method.ParameterList.Parameters.Select(p => p.ToString())); var body = (BlockSyntax)method.Body!; Assert.Single(body.Statements); // the for-loop; no return appended (void) Assert.Single(calls); Assert.Equal("Extract(ref acc, n);", calls[0].ToString()); VerifyZeroDiagnostics(tree); } // --------------------------------------------------------------------- // Pairwise (141..147): two declared-inside locals both written inside and // read after — the promoted signature returns the tuple `(int, int)` and // the call site deconstructs it; the new body gains `return (x, y);`. // --------------------------------------------------------------------- [Fact] public void Pairwise_selection_generates_tuple_return_and_deconstruction_call() { var (method, calls, tree) = Generate(141, 147); Assert.Equal("(int, int)", method.ReturnType.ToString()); Assert.Equal(new[] { "int n" }, method.ParameterList.Parameters.Select(p => p.ToString())); var body = (BlockSyntax)method.Body!; Assert.Equal(4, body.Statements.Count); // x, y, for-loop, appended tuple return Assert.Equal("return (x, y);", body.Statements[^1].ToString()); Assert.Single(calls); Assert.Equal("var (x, y) = Extract(n);", calls[0].ToString()); VerifyZeroDiagnostics(tree); } // --------------------------------------------------------------------- // The unified diff, pinned exactly for a minimal edit (hunk header, // context, deletion before insertion). // --------------------------------------------------------------------- [Fact] public void UnifiedDiff_pins_the_hunk_format() { var diff = UnifiedDiff.Diff( "a\nb\nc\n", "a\nX\nc\nd\n", "Demo.cs", "Demo.cs (generated)"); Assert.Equal( "--- Demo.cs\n" + "+++ Demo.cs (generated)\n" + "@@ -1,3 +1,4 @@\n" + " a\n" + "-b\n" + "+X\n" + " c\n" + "+d\n", diff); } // --------------------------------------------------------------------- // Identical texts produce a header with no hunks. // --------------------------------------------------------------------- [Fact] public void UnifiedDiff_of_identical_texts_is_header_only() { Assert.Equal("--- a\n+++ b\n", UnifiedDiff.Diff("same\n", "same\n", "a", "b")); } // --------------------------------------------------------------------- private static (MethodDeclarationSyntax Method, List Calls, SyntaxTree Tree) Generate(int startLine, int endLine) { var (tree, compilation) = DemoFixture.Load(); var model = compilation.GetSemanticModel(tree); var resolved = SelectionResolver.Resolve(tree, startLine, endLine); Assert.True(resolved.Succeeded, resolved.Error); var suggestion = DataFlowClassifier.Classify(model, resolved); var signature = SignatureBuilder.Build(model, resolved, suggestion); var refactoring = RefactoringGenerator.Generate(model, resolved, signature); return (refactoring.NewMethod, refactoring.CallStatements.ToList(), refactoring.TransformedTree); } /// /// The yak's round-trip check: the transformed tree must compile with /// ZERO diagnostics under the same scratch compilation the input file /// compiles under. /// private static void VerifyZeroDiagnostics(SyntaxTree tree) { var compilation = CompilationLoader.CreateCompilation(tree, "DemoFixture"); var diagnostics = compilation.GetDiagnostics().ToList(); Assert.True(diagnostics.Count == 0, string.Join("\n", diagnostics)); } }