diff --git a/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs b/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs
index 611ef98..1db5a3e 100644
--- a/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs
+++ b/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs
@@ -57,6 +57,27 @@ public static class Patterns
///
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.
+
+ /// fs.AllPaths — union of files and directories across the mocked file system.
+ public const string MockFileSystemAllPaths = "MockFileSystem.AllPaths";
+
+ /// fs.AllFiles — every mocked file path.
+ public const string MockFileSystemAllFiles = "MockFileSystem.AllFiles";
+
+ /// fs.AllDirectories — every mocked directory path.
+ public const string MockFileSystemAllDirectories = "MockFileSystem.AllDirectories";
+
+ /// fs.AllDrives — every mocked drive name.
+ public const string MockFileSystemAllDrives = "MockFileSystem.AllDrives";
+
// ── MockFileData property access ──────────────────────────────────────
/// A read access to a MockFileData property.
diff --git a/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs b/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs
index 055731a..63932a5 100644
--- a/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs
+++ b/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs
@@ -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;
}
@@ -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 parameters = constructor.Parameters;
diff --git a/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs b/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs
index bebe718..e62fafc 100644
--- a/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs
+++ b/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs
@@ -135,6 +135,48 @@ public async Task AddDrive_WithTotalSize_RegistersDriveWithSize()
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"));
+ fs.AddFile("/b/two.txt", new MockFileData("2"));
+
+ bool sawOne = false;
+ bool sawTwo = false;
+ foreach (string path in fs.AllFiles)
+ {
+ 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");
+ fs.AddDirectory("/b/y");
+
+ bool sawX = false;
+ bool sawY = false;
+ foreach (string path in fs.AllDirectories)
+ {
+ sawX |= path.EndsWith("x", StringComparison.Ordinal);
+ sawY |= path.EndsWith("y", StringComparison.Ordinal);
+ }
+
+ await That(sawX).IsTrue();
+ await That(sawY).IsTrue();
+ }
+
private static IDriveInfo? FindDriveByName(IDriveInfo[] drives, string prefix)
{
foreach (IDriveInfo drive in drives)
diff --git a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs
index 959a2cc..64d965d 100644
--- a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs
+++ b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs
@@ -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 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 Read(IMockFileDataAccessor accessor) => {|#0:accessor.AllFiles|};
+ }
+ """;
+
+ await Verifier.VerifyAnalyzerAsync(
+ source,
+ Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0));
+ }
+
[Theory]
[InlineData("TextContents")]
[InlineData("Contents")]
diff --git a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs
index 5962e04..c1d8540 100644
--- a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs
+++ b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs
@@ -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 Read(MockFileSystem fs) => {|#0:fs.{{property}}|};
+ }
+ """;
+
+ await Verifier.VerifyCodeFixAsync(
+ source,
+ Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0),
+ source);
+ }
}
}