em 01: scaffold ExtractMethod tool + Demo fixture
- tools/ExtractMethod: net10.0 console app (Microsoft.CodeAnalysis.CSharp 5.9.0, pinned latest stable). CLI: <file.cs> <startLine> <endLine>. Parses the file, builds a scratch compilation with refs from TRUSTED_PLATFORM_ASSEMBLIES, and reports how many whole statements the line range covers (clean error otherwise; exit codes: 0 ok, 1 resolution error, 2 usage). - Tooling/CompilationLoader: shared parse + compilation path for CLI and tests (tests exercise the exact loading path the CLI uses). - Tooling/SelectionResolver: snaps a 1-based inclusive line range to whole statements in the enclosing method body block; boundary checks never split a statement; nested/blank-line ranges handled cleanly. - tests/: ExtractMethod/Fixtures/Demo.cs checked-in fixture exercising every bucket of the parent spec (read-only local, written+read-later return, scratch local, param read, field+property access, indexer + method invocation, multi-statement range incl. a for-loop); excluded from project compilation, copied to output as data. - Tests: Demo.cs compiles with no diagnostics; range 68..72 resolves to 2 statements (LocalDeclarationStatement, ForStatement); AnalyzeDataFlow succeeds on the fixture's for-loop node; mid-statement range fails cleanly. 30/30 green.
This commit is contained in:
@@ -21,6 +21,19 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Before\Before.csproj" />
|
||||
<ProjectReference Include="..\..\src\After\After.csproj" />
|
||||
<ProjectReference Include="..\..\tools\ExtractMethod\ExtractMethod.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The ExtractMethod fixtures are DATA for the Roslyn tool, not source:
|
||||
they are excluded from this project's compilation (the scratch
|
||||
compilation gives the tool the exact checked-in text) and copied to
|
||||
the output dir so tests can find them by path. -->
|
||||
<Compile Remove="ExtractMethod\Fixtures\**\*.cs" />
|
||||
<!-- Rebase the copy target to Fixtures/ in the output dir: the tool's
|
||||
apphost (the Executable 'ExtractMethod') is copied here too, and a
|
||||
folder named ExtractMethod/ would collide with that file. -->
|
||||
<Content Include="ExtractMethod\Fixtures\**\*.cs" Link="Fixtures\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,102 @@
|
||||
using ExtractMethod.Tooling;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace BeforeAfter.Tests.ExtractMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for yak em 01: the scaffold (console tool) and the checked-in demo
|
||||
/// fixture. The fixture is DATA (excluded from compilation, copied to the
|
||||
/// output dir), and it is parsed with the tool's own <see cref="CompilationLoader"/>
|
||||
/// so these tests exercise the exact loading path the CLI uses.
|
||||
/// </summary>
|
||||
public class DemoFixtureTests
|
||||
{
|
||||
private static readonly string FixturePath =
|
||||
Path.Combine(AppContext.BaseDirectory, "Fixtures", "Demo.cs");
|
||||
|
||||
private static (SyntaxTree Tree, CSharpCompilation Compilation) LoadFixture()
|
||||
{
|
||||
Assert.True(File.Exists(FixturePath), $"fixture missing at {FixturePath}");
|
||||
var tree = CompilationLoader.ParseFile(FixturePath);
|
||||
var compilation = CompilationLoader.CreateCompilation(tree, "DemoFixture");
|
||||
return (tree, compilation);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (a) The fixture is pristine under the scratch compilation: if this
|
||||
// fails, the fixture (or the TRUSTED_PLATFORM_ASSEMBLIES reference
|
||||
// loading) is broken and every later yak would analyze garbage.
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void Demo_compiles_with_no_diagnostics()
|
||||
{
|
||||
var (_, compilation) = LoadFixture();
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (b) A known line range snaps to the expected whole statements.
|
||||
// Lines 68..72 of Demo.cs are `int total = 0;` followed by the whole
|
||||
// for-loop (for keyword .. closing brace) inside Summarize — exactly
|
||||
// the "multi-statement range incl. a for-loop" shape of the spec.
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void Known_range_resolves_to_expected_statements()
|
||||
{
|
||||
var (tree, _) = LoadFixture();
|
||||
|
||||
var report = SelectionResolver.Resolve(tree, 68, 72);
|
||||
|
||||
Assert.True(report.Succeeded, report.Error);
|
||||
Assert.Equal(2, report.Count);
|
||||
Assert.Equal(
|
||||
new[] { SyntaxKind.LocalDeclarationStatement, SyntaxKind.ForStatement },
|
||||
report.Kinds);
|
||||
Assert.Equal("Summarize", report.Method?.Identifier.ValueText);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// (c) The semantic model's data-flow analysis works on the fixture's
|
||||
// for-loop node — the foundation every classification in yak 02
|
||||
// builds on. (AnalyzeDataFlow on the loop itself walks the bound loop
|
||||
// body; Succeeded == false would mean the scratch compilation or the
|
||||
// selected node cannot be bound.)
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void Fixture_for_loop_supports_data_flow_analysis()
|
||||
{
|
||||
var (tree, compilation) = LoadFixture();
|
||||
|
||||
var forStatement = tree.GetRoot()
|
||||
.DescendantNodes()
|
||||
.OfType<ForStatementSyntax>()
|
||||
.Single(n => EnclosingMethodName(n) == "Summarize");
|
||||
|
||||
var dataFlow = compilation.GetSemanticModel(tree).AnalyzeDataFlow(forStatement);
|
||||
|
||||
Assert.True(dataFlow.Succeeded);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Acceptance also demands "clean error otherwise": selecting only the
|
||||
// for-loop header line (line 69) splits the statement and must fail with
|
||||
// a readable reason instead of a crash or a silent wrong count.
|
||||
// ---------------------------------------------------------------------
|
||||
[Fact]
|
||||
public void Range_ending_mid_statement_fails_cleanly()
|
||||
{
|
||||
var (tree, _) = LoadFixture();
|
||||
|
||||
var report = SelectionResolver.Resolve(tree, 69, 69);
|
||||
|
||||
Assert.False(report.Succeeded);
|
||||
Assert.NotNull(report.Error);
|
||||
Assert.Contains("no whole", report.Error);
|
||||
}
|
||||
|
||||
private static string? EnclosingMethodName(SyntaxNode node) =>
|
||||
node.AncestorsAndSelf().OfType<MethodDeclarationSyntax>().FirstOrDefault()?.Identifier.ValueText;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Demo fixture for the extract-method micro-tool (tools/ExtractMethod).
|
||||
//
|
||||
// This file is DATA, not project source: the test project excludes it from
|
||||
// compilation (see BeforeAfter.Tests.csproj) and runs Roslyn over its raw
|
||||
// text, so the tool sees exactly what is checked in here. It uses explicit
|
||||
// usings because the scratch compilation has no implicit usings.
|
||||
//
|
||||
// Bucket map (the classification yak 02 works from):
|
||||
// bucket where example
|
||||
// local, read-only (param) ScoreReads seed, score
|
||||
// written + read-later (return) Summarize, Heaviest total, message, best
|
||||
// scratch local (local) Summarize for-loop i
|
||||
// enclosing-method param (param) ScoreReads, Summarize bonus, limit
|
||||
// field access (extract-first) ScoreReads _seed (optional)
|
||||
// property access (extract-first) Summarize Scale (optional)
|
||||
// indexer access (extract-first) Heaviest widgets[i]
|
||||
// method invocation (extract-first, flagged) Heaviest best.Bigger(...)
|
||||
// multi-statement range incl. for Summarize total + for-loop
|
||||
//
|
||||
// Tests pin EXACT line numbers of the statements they select. Keep the
|
||||
// formatting stable; when a line must change, update DemoFixtureTests too.
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BeforeAfter.Tests.ExtractMethod.Fixtures;
|
||||
|
||||
/// <summary>
|
||||
/// Small first-class citizen of the extract-method tool: every method below
|
||||
/// shows one or more of the classification "buckets" the tool must report.
|
||||
/// </summary>
|
||||
public class Demo
|
||||
{
|
||||
// Own-class state: reading it from a selection is "extract-first optional"
|
||||
// (a same-class extracted method can still see these fields). Initialized
|
||||
// here so the fixture compiles with zero diagnostics (CS0649 otherwise).
|
||||
private int _seed = 5;
|
||||
|
||||
// Own-class property: like a field, accessible from a same-class method,
|
||||
// so hoisting it is optional in the report.
|
||||
public int Scale { get; set; } = 1;
|
||||
|
||||
/// <summary>Small local type so the scratch compilation binds.</summary>
|
||||
public sealed record Widget(int Weight, int Count, string Label)
|
||||
{
|
||||
/// <summary>Instance method used to exercise the invocation bucket.</summary>
|
||||
public Widget Bigger(Widget other) =>
|
||||
Weight >= other.Weight ? this : other;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bucket: enclosing-method parameter read (bonus => param), local
|
||||
/// read-only (seed, score => param), own-class field access
|
||||
/// (_seed => extract-first, optional). Ends with a return expression.
|
||||
/// </summary>
|
||||
public int ScoreReads(int bonus)
|
||||
{
|
||||
int seed = _seed;
|
||||
int score = seed * 2;
|
||||
return score + bonus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bucket: written + read-later locals (total, message => return),
|
||||
/// scratch local (i => local), own-class property access (Scale =>
|
||||
/// extract-first optional), multi-statement range incl. a for-loop.
|
||||
/// </summary>
|
||||
public string Summarize(int limit)
|
||||
{
|
||||
int total = 0;
|
||||
for (int i = 0; i < limit; i++)
|
||||
{
|
||||
total += i;
|
||||
}
|
||||
|
||||
int scaled = total * Scale;
|
||||
string message = "sum=" + scaled;
|
||||
return message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bucket: indexer access (widgets[i] on List<T> => extract-first),
|
||||
/// method invocation (Bigger => extract-first, flagged because hoisting
|
||||
/// changes how often it is evaluated), local written + read-later
|
||||
/// (best => return).
|
||||
/// </summary>
|
||||
public Widget Heaviest(List<Widget> widgets, int count)
|
||||
{
|
||||
Widget best = widgets[0];
|
||||
for (int i = 1; i < count; i++)
|
||||
{
|
||||
best = best.Bigger(widgets[i]);
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user