using ExtractMethod.Tooling; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace BeforeAfter.Tests.ExtractMethod; /// /// Tests for yak em 02: the data-flow classifier (buckets: params / returns / /// locals) over the checked-in Demo.cs fixture. Line anchors are the exact /// fixture lines pinned by em 01 (see Demo.cs header comment): /// ScoreReads body 56..58, Summarize body 68..76 (for-loop 69..72), /// Heaviest body 87..93. /// public class DataFlowClassificationTests { // --------------------------------------------------------------------- // in-param bucket (ScoreReads, full body 56..58). // ReadInside = {_seed→field, seed, score, bonus}; the classifier must // report the reads that are locals/params — seed/score/bonus — as in- // params, and must NOT leak `this` (the implicit this-read behind the // _seed field access; filtered via IParameterSymbol.IsThis) into params, // nor the _seed field itself (decision #6 keeps fields for em 03). // WrittenInside = {seed, score}; no tail, trailing return is an expression // (score + bonus) which v1 cannot name -> no returns, both written vars // are scratch locals. // --------------------------------------------------------------------- [Fact] public void ScoreReads_full_body_reads_report_three_in_params_and_no_returns() { var suggestion = ResolveAndClassify(56, 58); Assert.Equal(new[] { "bonus", "score", "seed" }, suggestion.Params.Select(p => p.Name)); // None is reassigned inside (declaration initializers are not write-backs) -> no ref. Assert.All(suggestion.Params, p => Assert.False(p.ByRef)); Assert.Empty(suggestion.Returns); // v1 reports the read locals ALSO as scratch locals (decision #6 over- // reports local reads); em 03 dedupes them into the signature. Assert.Equal(new[] { "score", "seed" }, suggestion.Locals); } // --------------------------------------------------------------------- // multi-statement range case (Summarize 68..72: LocalDeclarationStatement // + ForStatement): the resolution, the selection data flow AND the tail // data flow all go through the two-argument overload with Succeeded=true // (the synth-block trick from parent decision #3 is the DEAD path — see // Test for it below). Buckets: total written inside + read at line 74 in // the tail -> return; for-var i never read after -> local. // --------------------------------------------------------------------- [Fact] public void Summarize_multi_statement_range_flows_via_two_argument_overload_and_buckets_total_return_i_local() { var (tree, compilation) = DemoFixture.Load(); var model = compilation.GetSemanticModel(tree); var report = SelectionResolver.Resolve(tree, 68, 72); Assert.True(report.Succeeded, report.Error); var selectionFlow = DataFlowClassifier.AnalyzeSelectionFlow(model, report); Assert.True(selectionFlow.Succeeded, "two-argument AnalyzeDataFlow must succeed on a multi-statement selection"); var tailFlow = DataFlowClassifier.AnalyzeTailFlow(model, report); Assert.NotNull(tailFlow); Assert.True(tailFlow.Succeeded, "two-argument AnalyzeDataFlow must succeed on the (multi-statement) tail region"); var suggestion = DataFlowClassifier.Classify(model, report); Assert.Equal(new[] { "total" }, suggestion.Returns.Select(r => r.Name)); Assert.Equal(new[] { "int" }, suggestion.Returns.Select(r => r.Type)); Assert.Equal(new[] { "i" }, suggestion.Locals); // limit is read in the for-header (68..72) — a plain by-value in-param. var limit = Assert.Single(suggestion.Params.Where(p => p.Name == "limit")); Assert.False(limit.ByRef); } // --------------------------------------------------------------------- // return + ref-param buckets (Summarize, full body 68..76). // Returns: the tail is empty, so the tail-flow rule yields nothing; the // selection ends with `return message;` -> message is the trailing-return // candidate (parent spec). Locals: everything else written inside: // {i, scaled, total}. // Params (all ReadInside locals/params): the differential ref check — // total is REASSIGNED inside (total += i at line 71) -> ByRef; scaled and // limit are never reassigned (declaration write only) -> plain in-params. // --------------------------------------------------------------------- [Fact] public void Summarize_full_body_returns_trailing_message_and_flags_reassigned_total_as_ref() { var suggestion = ResolveAndClassify(68, 76); Assert.Equal(new[] { "message" }, suggestion.Returns.Select(r => r.Name)); Assert.Equal(new[] { "i", "scaled", "total" }, suggestion.Locals); var total = Assert.Single(suggestion.Params.Where(p => p.Name == "total")); Assert.True(total.ByRef, "total is reassigned inside (total += i) -> the extraction owes a write-back"); var scaled = Assert.Single(suggestion.Params.Where(p => p.Name == "scaled")); Assert.False(scaled.ByRef, "scaled is only declared inside, never reassigned"); var limit = Assert.Single(suggestion.Params.Where(p => p.Name == "limit")); Assert.False(limit.ByRef); } // --------------------------------------------------------------------- // return + extract-first isolation (Heaviest, full body 87..93). // Returns: `return best;` is the last statement -> best (trailing-return // rule again). Written inside but never read after: i -> local. // Params: reads are best/count/i/widgets; best and i are reassigned // inside -> ref. Nothing extract-first (widgets[i] indexer, Bigger // invocation — em 03 territory) may leak into any bucket: the params list // is exactly the four variable reads. // --------------------------------------------------------------------- [Fact] public void Heaviest_full_body_returns_best_locals_i_and_leaks_no_extract_first_names() { var suggestion = ResolveAndClassify(87, 93); Assert.Equal(new[] { "best" }, suggestion.Returns.Select(r => r.Name)); Assert.Equal(new[] { "i" }, suggestion.Locals); Assert.Equal(new[] { "best", "count", "i", "widgets" }, suggestion.Params.Select(p => p.Name)); Assert.Equal(new[] { true, false, true, false }, suggestion.Params.Select(p => p.ByRef)); } // --------------------------------------------------------------------- // single-statement fallback (ScoreReads, line 58 alone: `return ...;`). // The one-argument AnalyzeDataFlow overload is the live path for a single // statement. The trailing return is `score + bonus` — an expression, not // a simple name — so v1 reports no return candidate (composite return // expressions are extract-first shape for em 03) and no locals. // --------------------------------------------------------------------- [Fact] public void Single_statement_selection_uses_one_argument_overload_and_composite_trailing_return_is_not_nameable() { var (tree, compilation) = DemoFixture.Load(); var model = compilation.GetSemanticModel(tree); var report = SelectionResolver.Resolve(tree, 58, 58); Assert.True(report.Succeeded, report.Error); Assert.True(DataFlowClassifier.AnalyzeSelectionFlow(model, report).Succeeded); var suggestion = DataFlowClassifier.Classify(model, report); Assert.Equal(new[] { "bonus", "score" }, suggestion.Params.Select(p => p.Name)); Assert.All(suggestion.Params, p => Assert.False(p.ByRef)); Assert.Empty(suggestion.Returns); Assert.Empty(suggestion.Locals); } // --------------------------------------------------------------------- // DEAD PATH (parent decision #3, pinned so the choice is self-explanatory // to future readers): a synthetic BlockSyntax re-parents tree nodes that // are not in the tree, and AnalyzeDataFlow refuses with // ArgumentException("statements not within tree"). That is why the live // path uses the two-argument overload (both endpoints are real tree // nodes) — which also keeps symbol identity across selection and tail // analyses, the prerequisite for the written-inside ∩ read-after // intersection of the return bucket. // --------------------------------------------------------------------- [Fact] public void Synthetic_block_data_flow_throws_not_within_tree_so_the_two_argument_overload_stays_live() { var (tree, compilation) = DemoFixture.Load(); var model = compilation.GetSemanticModel(tree); var summarize = tree.GetRoot().DescendantNodes() .OfType() .Single(m => m.Identifier.ValueText == "Summarize"); var statements = summarize.Body!.Statements; // Same node instances, new (synthetic) parent — this is the shape // parent decision #3 proposed for multi-statement ranges and tails. var syntheticBlock = SyntaxFactory.Block(statements[2], statements[3], statements[4]); var ex = Assert.Throws(() => model.AnalyzeDataFlow(syntheticBlock)); Assert.Contains("not within tree", ex.Message, StringComparison.Ordinal); } private static ExtractionSuggestion ResolveAndClassify(int startLine, int endLine) { var (tree, compilation) = DemoFixture.Load(); var model = compilation.GetSemanticModel(tree); var report = SelectionResolver.Resolve(tree, startLine, endLine); Assert.True(report.Succeeded, report.Error); return DataFlowClassifier.Classify(model, report); } }