Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,27 @@ public static class Patterns
/// </summary>
public const string MockFileSystemAddDrive = "MockFileSystem.AddDrive";

// ── Enumeration properties (Phase 5.1) ────────────────────────────────
// These IMockFileDataAccessor properties enumerate the whole mocked file
// system. Testably has no direct equivalent — the natural replacements
// (Directory.EnumerateFiles/EnumerateDirectories, DriveInfo.GetDrives,
// etc.) need a root path or drive scope the analyzer cannot infer safely.
// Each property gets its own pattern id so manual migration is
// discoverable per call site; the code-fix provider intentionally
// registers no rewrite.

/// <summary><c>fs.AllPaths</c> — union of files and directories across the mocked file system.</summary>
public const string MockFileSystemAllPaths = "MockFileSystem.AllPaths";

/// <summary><c>fs.AllFiles</c> — every mocked file path.</summary>
public const string MockFileSystemAllFiles = "MockFileSystem.AllFiles";

/// <summary><c>fs.AllDirectories</c> — every mocked directory path.</summary>
public const string MockFileSystemAllDirectories = "MockFileSystem.AllDirectories";

/// <summary><c>fs.AllDrives</c> — every mocked drive name.</summary>
public const string MockFileSystemAllDrives = "MockFileSystem.AllDrives";

// ── MockFileData property access ──────────────────────────────────────

/// <summary>A read access to a <c>MockFileData</c> property.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,35 @@ symbols.MockFileDataAccessor is { } accessor

private static void AnalyzePropertyReference(OperationAnalysisContext context, TestableIoSymbols symbols)
{
if (symbols.MockFileData is null
|| context.Operation is not IPropertyReferenceOperation propertyRef)
if (context.Operation is not IPropertyReferenceOperation propertyRef)
{
return;
}

if (!SymbolEqualityComparer.Default.Equals(propertyRef.Property.ContainingType, symbols.MockFileData))
INamedTypeSymbol? containingType = propertyRef.Property.ContainingType;
if (containingType is null)
{
return;
}

// Phase 5.1: IMockFileDataAccessor enumeration properties (AllPaths,
// AllFiles, AllDirectories, AllDrives). These have no 1:1 Testably
// equivalent — manual migration only.
bool onMockFileSystem = SymbolEqualityComparer.Default.Equals(containingType, symbols.MockFileSystem);
bool onAccessor = symbols.MockFileDataAccessor is { } accessor
&& SymbolEqualityComparer.Default.Equals(containingType, accessor);
if (onMockFileSystem || onAccessor)
{
string? enumerationPattern = ClassifyEnumerationProperty(propertyRef.Property.Name);
if (enumerationPattern is not null)
{
Report(context, propertyRef.Syntax.GetLocation(), enumerationPattern);
return;
}
}

if (symbols.MockFileData is null
|| !SymbolEqualityComparer.Default.Equals(containingType, symbols.MockFileData))
{
return;
}
Expand Down Expand Up @@ -249,6 +271,15 @@ private static void AnalyzePropertyReference(OperationAnalysisContext context, T
_ => null,
};

private static string? ClassifyEnumerationProperty(string propertyName) => propertyName switch
{
"AllPaths" => Patterns.MockFileSystemAllPaths,
"AllFiles" => Patterns.MockFileSystemAllFiles,
"AllDirectories" => Patterns.MockFileSystemAllDirectories,
"AllDrives" => Patterns.MockFileSystemAllDrives,
_ => null,
};

private static string? ClassifyMockFileSystemConstructor(IMethodSymbol constructor)
{
ImmutableArray<IParameterSymbol> parameters = constructor.Parameters;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,48 @@
await That(drive!.TotalSize).IsEqualTo(totalSize);
}

[Fact]
public async Task AllFiles_EnumeratesEveryAddedFile()
{
// Phase 5.1 manual-review fixture: Testably has no AllFiles equivalent. The
// migration target depends on the user's drive layout — Directory.EnumerateFiles
// against the right root, or DriveInfo.GetDrives() + SelectMany for multi-drive
// setups. The playground keeps the parity baseline so a human can decide.
MockFileSystem fs = new();
fs.AddFile("/a/one.txt", new MockFileData("1"));

Check warning on line 146 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

System.IO.Abstractions MockFileSystem should be migrated to Testably.Abstractions.

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qhhKjGPGjB1w4M8TM&open=AZ4qhhKjGPGjB1w4M8TM&pullRequest=14
fs.AddFile("/b/two.txt", new MockFileData("2"));

Check warning on line 147 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

System.IO.Abstractions MockFileSystem should be migrated to Testably.Abstractions.

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qhhKjGPGjB1w4M8TN&open=AZ4qhhKjGPGjB1w4M8TN&pullRequest=14

bool sawOne = false;
bool sawTwo = false;
foreach (string path in fs.AllFiles)

Check warning on line 151 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

System.IO.Abstractions MockFileSystem should be migrated to Testably.Abstractions.

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qhhKjGPGjB1w4M8TO&open=AZ4qhhKjGPGjB1w4M8TO&pullRequest=14
{
sawOne |= path.EndsWith("one.txt", StringComparison.Ordinal);
sawTwo |= path.EndsWith("two.txt", StringComparison.Ordinal);
}

await That(sawOne).IsTrue();
await That(sawTwo).IsTrue();
}

[Fact]
public async Task AllDirectories_EnumeratesEveryAddedDirectory()
{
MockFileSystem fs = new();
fs.AddDirectory("/a/x");

Check warning on line 165 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

System.IO.Abstractions MockFileSystem should be migrated to Testably.Abstractions.

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qhhKjGPGjB1w4M8TI&open=AZ4qhhKjGPGjB1w4M8TI&pullRequest=14
fs.AddDirectory("/b/y");

Check warning on line 166 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

System.IO.Abstractions MockFileSystem should be migrated to Testably.Abstractions.

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qhhKjGPGjB1w4M8TJ&open=AZ4qhhKjGPGjB1w4M8TJ&pullRequest=14

bool sawX = false;
bool sawY = false;
foreach (string path in fs.AllDirectories)

Check warning on line 170 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

System.IO.Abstractions MockFileSystem should be migrated to Testably.Abstractions.

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qhhKjGPGjB1w4M8TK&open=AZ4qhhKjGPGjB1w4M8TK&pullRequest=14
{
sawX |= path.EndsWith("x", StringComparison.Ordinal);

Check warning on line 172 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'string.EndsWith(char)' instead of 'string.EndsWith(string)' when you have a string with a single char

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qhhKjGPGjB1w4M8TF&open=AZ4qhhKjGPGjB1w4M8TF&pullRequest=14
sawY |= path.EndsWith("y", StringComparison.Ordinal);

Check warning on line 173 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'string.EndsWith(char)' instead of 'string.EndsWith(string)' when you have a string with a single char

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qhhKjGPGjB1w4M8TG&open=AZ4qhhKjGPGjB1w4M8TG&pullRequest=14
}

await That(sawX).IsTrue();
await That(sawY).IsTrue();
}

private static IDriveInfo? FindDriveByName(IDriveInfo[] drives, string prefix)
{
foreach (IDriveInfo drive in drives)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,49 @@ await Verifier.VerifyAnalyzerAsync(
Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0));
}

[Theory]
[InlineData("AllPaths")]
[InlineData("AllFiles")]
[InlineData("AllDirectories")]
[InlineData("AllDrives")]
public async Task EnumerationProperty_OnMockFileSystem_ShouldBeFlagged(string property)
{
string source = $$"""
using System.Collections.Generic;
using System.IO.Abstractions.TestingHelpers;

public class C
{
public IEnumerable<string> Read(MockFileSystem fs) => {|#0:fs.{{property}}|};
}
""";

await Verifier.VerifyAnalyzerAsync(
source,
Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0));
}

[Fact]
public async Task EnumerationProperty_OnAccessorInterface_ShouldBeFlagged()
{
// AllFiles is declared on IMockFileDataAccessor — when the receiver is the
// interface type itself, the property symbol's containing type points at the
// interface, which the analyzer must still recognise.
const string source = """
using System.Collections.Generic;
using System.IO.Abstractions.TestingHelpers;

public class C
{
public IEnumerable<string> Read(IMockFileDataAccessor accessor) => {|#0:accessor.AllFiles|};
}
""";

await Verifier.VerifyAnalyzerAsync(
source,
Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0));
}

[Theory]
[InlineData("TextContents")]
[InlineData("Contents")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,5 +678,32 @@ await Verifier.VerifyCodeFixAsync(
Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0),
source);
}

[Theory]
[InlineData("AllPaths")]
[InlineData("AllFiles")]
[InlineData("AllDirectories")]
[InlineData("AllDrives")]
public async Task EnumerationProperty_HasNoFix(string property)
{
// Testably has no 1:1 equivalent for the IMockFileDataAccessor enumeration
// properties. The natural replacements (Directory.EnumerateFiles, etc.)
// require a root path or drive scope the analyzer cannot infer safely, so
// the fix dispatcher intentionally falls through with no rewrite.
string source = $$"""
using System.Collections.Generic;
using System.IO.Abstractions.TestingHelpers;

public class C
{
public IEnumerable<string> Read(MockFileSystem fs) => {|#0:fs.{{property}}|};
}
""";

await Verifier.VerifyCodeFixAsync(
source,
Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0),
source);
}
}
}
Loading