diff --git a/Source/Testably.Abstractions.Migration.Analyzers.CodeFixers/SystemIOAbstractionsCodeFixProvider.cs b/Source/Testably.Abstractions.Migration.Analyzers.CodeFixers/SystemIOAbstractionsCodeFixProvider.cs index d1b5244..1b474a5 100644 --- a/Source/Testably.Abstractions.Migration.Analyzers.CodeFixers/SystemIOAbstractionsCodeFixProvider.cs +++ b/Source/Testably.Abstractions.Migration.Analyzers.CodeFixers/SystemIOAbstractionsCodeFixProvider.cs @@ -89,6 +89,10 @@ await TryRegisterFilesCtorFixAsync(context, diagnostic, node, pattern) case Patterns.MockFileDataPropertyWrite: TryRegisterPropertyWriteFix(context, diagnostic, node); break; + case Patterns.MockFileSystemAddDrive: + await TryRegisterAddDriveFixAsync(context, diagnostic, node) + .ConfigureAwait(false); + break; } } } @@ -1229,6 +1233,273 @@ private static bool TryMatchOneShotGetFileWrite( _ => null, }; + // ── Pattern: MockFileSystem.AddDrive ───────────────────────────────────── + + private static async Task TryRegisterAddDriveFixAsync( + CodeFixContext context, Diagnostic diagnostic, SyntaxNode node) + { + InvocationExpressionSyntax? invocation = node.FirstAncestorOrSelf(); + if (invocation?.Expression is not MemberAccessExpressionSyntax memberAccess + || invocation.ArgumentList.Arguments.Count != 2) + { + return; + } + + // The rewrite emits `.WithDrive(...)`. WithDrive is Testably-only, so + // we must swap the using as part of the fix. The semantic check confirms the + // receiver is currently typed as TestableIO MockFileSystem; the syntactic check + // below confirms the declaration's type syntax can actually be retargeted by + // the using swap (alias- or fully-qualified declarations stay bound to + // TestableIO after the swap, so the rewrite would produce non-compiling code). + SemanticModel? semanticModel = await context.Document + .GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + if (semanticModel is null + || !IsConcreteMockFileSystemReceiver(memberAccess.Expression, semanticModel) + || !IsRetargetableMockFileSystemReceiver(memberAccess.Expression, semanticModel)) + { + return; + } + + if (!TryClassifyMockDriveDataInitializer(invocation.ArgumentList.Arguments[1].Expression, out _)) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + Resources.TestablyM001CodeFixTitle, + ct => ApplyAddDriveRewriteAsync(context.Document, diagnostic, ct), + equivalenceKey: Patterns.MockFileSystemAddDrive), + diagnostic); + } + + private static bool IsRetargetableMockFileSystemReceiver( + ExpressionSyntax receiver, SemanticModel semanticModel) + { + // Direct construction: the construction expression itself is what the swap + // retargets, so re-use the constructor-level gate. + if (receiver is BaseObjectCreationExpressionSyntax creation) + { + return HasUnqualifiedMockFileSystemTypeName(creation); + } + + // Symbol references (locals, parameters, fields, properties, method results): + // inspect the declared type syntax. The swap only retargets unqualified + // `MockFileSystem` (or `var` resolved from an unqualified initializer). Alias- + // qualified (`TestableIo.MockFileSystem`) and fully-qualified + // (`System.IO.Abstractions.TestingHelpers.MockFileSystem`) declarations stay + // bound to TestableIO after the swap, so the rewrite would emit `WithDrive` on + // the old MockFileSystem and fail to compile. + ISymbol? symbol = semanticModel.GetSymbolInfo(receiver).Symbol; + if (symbol is null || symbol.DeclaringSyntaxReferences.Length == 0) + { + return false; + } + + foreach (SyntaxReference declRef in symbol.DeclaringSyntaxReferences) + { + TypeSyntax? declaredType = declRef.GetSyntax() switch + { + VariableDeclaratorSyntax v => (v.Parent as VariableDeclarationSyntax)?.Type, + ParameterSyntax p => p.Type, + PropertyDeclarationSyntax pd => pd.Type, + MethodDeclarationSyntax md => md.ReturnType, + _ => null, + }; + + if (declaredType is null || !IsUnqualifiedMockFileSystemTypeSyntax(declaredType)) + { + return false; + } + } + + return true; + } + + private static bool IsUnqualifiedMockFileSystemTypeSyntax(TypeSyntax typeSyntax) + => typeSyntax switch + { + IdentifierNameSyntax => true, + NullableTypeSyntax nullable => IsUnqualifiedMockFileSystemTypeSyntax(nullable.ElementType), + _ => false, + }; + + private static async Task ApplyAddDriveRewriteAsync( + Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + SyntaxNode? root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is not CompilationUnitSyntax compilationUnit) + { + return document; + } + + SyntaxNode? node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + InvocationExpressionSyntax? invocation = node?.FirstAncestorOrSelf(); + if (invocation?.Expression is not MemberAccessExpressionSyntax memberAccess + || invocation.ArgumentList.Arguments.Count != 2) + { + return document; + } + + ArgumentSyntax driveNameArg = invocation.ArgumentList.Arguments[0]; + ExpressionSyntax driveDataExpr = invocation.ArgumentList.Arguments[1].Expression; + if (!TryClassifyMockDriveDataInitializer(driveDataExpr, + out List? assignments)) + { + return document; + } + + InvocationExpressionSyntax replacement = BuildWithDriveInvocation( + memberAccess.Expression, driveNameArg, assignments); + compilationUnit = compilationUnit.ReplaceNode(invocation, replacement.WithTriviaFrom(invocation)); + compilationUnit = SwapToTestablyUsing(compilationUnit); + return document.WithSyntaxRoot(compilationUnit); + } + + private static bool TryClassifyMockDriveDataInitializer( + ExpressionSyntax driveDataExpr, + out List? assignments) + { + assignments = null; + + ArgumentListSyntax? argumentList; + InitializerExpressionSyntax? initializer; + switch (driveDataExpr) + { + case ObjectCreationExpressionSyntax explicitCreation: + argumentList = explicitCreation.ArgumentList; + initializer = explicitCreation.Initializer; + break; + case ImplicitObjectCreationExpressionSyntax implicitCreation: + argumentList = implicitCreation.ArgumentList; + initializer = implicitCreation.Initializer; + break; + default: + return false; + } + + // Reject ctor overloads with arguments (e.g. the MockDriveData copy ctor) — + // they have no 1:1 mapping to WithDrive's lambda surface. + if (argumentList is { Arguments.Count: > 0, }) + { + return false; + } + + assignments = []; + if (initializer is null) + { + return true; + } + + foreach (ExpressionSyntax expression in initializer.Expressions) + { + if (expression is not AssignmentExpressionSyntax assignment + || assignment.Left is not IdentifierNameSyntax property + || MapMockDriveDataProperty(property.Identifier.Text) is null) + { + assignments = null; + return false; + } + + assignments.Add(assignment); + } + + return true; + } + + private static InvocationExpressionSyntax BuildWithDriveInvocation( + ExpressionSyntax receiver, + ArgumentSyntax driveNameArg, + List assignments) + { + MemberAccessExpressionSyntax withDriveAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + receiver, + SyntaxFactory.IdentifierName("WithDrive")); + + // Strip NameColon from the kept drive-name argument: TestableIO uses + // `name`, Testably uses `drive` — positional binding is the safe shape. + ArgumentSyntax nameArg = driveNameArg.WithNameColon(null); + + if (assignments.Count == 0) + { + return SyntaxFactory.InvocationExpression( + withDriveAccess, + SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(nameArg))); + } + + SimpleLambdaExpressionSyntax lambda = BuildWithDriveLambda(assignments); + return SyntaxFactory.InvocationExpression( + withDriveAccess, + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList( + new[] { nameArg, SyntaxFactory.Argument(lambda), }))); + } + + private static SimpleLambdaExpressionSyntax BuildWithDriveLambda( + List assignments) + { + // Avoid shadowing identifiers used in any of the initializer RHS expressions. + string parameterName = PickFreshDriveLambdaParameterName(assignments); + ExpressionSyntax body = SyntaxFactory.IdentifierName(parameterName); + foreach (AssignmentExpressionSyntax assignment in assignments) + { + string propertyName = ((IdentifierNameSyntax)assignment.Left).Identifier.Text; + string setter = MapMockDriveDataProperty(propertyName)!; + body = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + body, + SyntaxFactory.IdentifierName(setter)), + SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(assignment.Right.WithoutTrivia())))); + } + + return SyntaxFactory.SimpleLambdaExpression( + SyntaxFactory.Parameter(SyntaxFactory.Identifier(parameterName)), + body); + } + + private static string PickFreshDriveLambdaParameterName(List assignments) + { + HashSet used = []; + foreach (AssignmentExpressionSyntax assignment in assignments) + { + foreach (IdentifierNameSyntax id in assignment.Right.DescendantNodesAndSelf().OfType()) + { + used.Add(id.Identifier.Text); + } + } + + string[] candidates = ["d", "drive", "driveBuilder",]; + foreach (string candidate in candidates) + { + if (!used.Contains(candidate)) + { + return candidate; + } + } + + for (int i = 1;; i++) + { + string n = $"d{i}"; + if (!used.Contains(n)) + { + return n; + } + } + } + + private static string? MapMockDriveDataProperty(string propertyName) => propertyName switch + { + "TotalSize" => "SetTotalSize", + "IsReady" => "SetIsReady", + "DriveFormat" => "SetDriveFormat", + "DriveType" => "SetDriveType", + // AvailableFreeSpace, TotalFreeSpace and VolumeLabel have no IStorageDrive + // setter equivalent — fall through to manual review. + _ => null, + }; + // ── Shared: using-directive swap ───────────────────────────────────────── private static CompilationUnitSyntax SwapToTestablyUsing(CompilationUnitSyntax compilationUnit) diff --git a/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs b/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs index e2a44f9..611ef98 100644 --- a/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs +++ b/Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs @@ -51,6 +51,12 @@ public static class Patterns /// accessor.FileExists(path). public const string AccessorFileExists = "accessor.FileExists"; + /// + /// fs.AddDrive(name, mockDriveData); rewrites to + /// fs.WithDrive(name, d => d.SetTotalSize(...).SetIsReady(...)). + /// + public const string MockFileSystemAddDrive = "MockFileSystem.AddDrive"; + // ── 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 598ce4d..055731a 100644 --- a/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs +++ b/Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs @@ -230,9 +230,13 @@ private static void AnalyzePropertyReference(OperationAnalysisContext context, T && invocation.TargetMethod.Parameters[0].Type.SpecialType == SpecialType.System_String && invocation.Arguments.Length == 1; - string pattern = isOneShotGetFile - ? (isWrite ? Patterns.MockFileDataPropertyWrite : Patterns.MockFileDataPropertyRead) - : (isWrite ? Patterns.MockFileDataCapturedReferenceWrite : Patterns.MockFileDataCapturedReferenceRead); + string pattern = (isOneShotGetFile, isWrite) switch + { + (true, true) => Patterns.MockFileDataPropertyWrite, + (true, false) => Patterns.MockFileDataPropertyRead, + (false, true) => Patterns.MockFileDataCapturedReferenceWrite, + (false, false) => Patterns.MockFileDataCapturedReferenceRead, + }; Report(context, propertyRef.Syntax.GetLocation(), pattern); } @@ -278,6 +282,7 @@ private static bool IsMockFileSystemOptions(IParameterSymbol parameter) "RemoveFile" => Patterns.AccessorRemoveFile, "MoveDirectory" => Patterns.AccessorMoveDirectory, "FileExists" => Patterns.AccessorFileExists, + "AddDrive" => Patterns.MockFileSystemAddDrive, _ => null, }; diff --git a/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs b/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs index 32b5ef5..bebe718 100644 --- a/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs +++ b/Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/AccessorMethodTests.cs @@ -112,4 +112,39 @@ public async Task RemoveFile_WithVerifyAccess_DeletesFile() await That(fs.FileExists("/a")).IsFalse(); } + + [Fact] + public async Task AddDrive_EmptyData_RegistersDrive() + { + MockFileSystem fs = new(); + fs.AddDrive("D:", new MockDriveData()); + + IDriveInfo? drive = FindDriveByName(fs.DriveInfo.GetDrives(), "D:"); + await That(drive).IsNotNull(); + } + + [Fact] + public async Task AddDrive_WithTotalSize_RegistersDriveWithSize() + { + const long totalSize = 1024L * 1024L; + MockFileSystem fs = new(); + fs.AddDrive("E:", new MockDriveData { TotalSize = totalSize }); + + IDriveInfo? drive = FindDriveByName(fs.DriveInfo.GetDrives(), "E:"); + await That(drive).IsNotNull(); + await That(drive!.TotalSize).IsEqualTo(totalSize); + } + + private static IDriveInfo? FindDriveByName(IDriveInfo[] drives, string prefix) + { + foreach (IDriveInfo drive in drives) + { + if (drive.Name.StartsWith(prefix, StringComparison.Ordinal)) + { + return drive; + } + } + + return null; + } } diff --git a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs index 33e902e..959a2cc 100644 --- a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs +++ b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsAnalyzerTests.cs @@ -124,6 +124,24 @@ await Verifier.VerifyAnalyzerAsync( Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0)); } + [Fact] + public async Task AddDrive_ShouldBeFlagged() + { + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs) + => {|#0:fs.AddDrive("D:", new MockDriveData())|}; + } + """; + + 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 be90798..5962e04 100644 --- a/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs +++ b/Tests/Testably.Abstractions.Migration.Tests/SystemIOAbstractionsCodeFixProviderTests.AccessorMethodTests.cs @@ -386,5 +386,297 @@ await Verifier.VerifyCodeFixAsync( Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), source); } + + [Fact] + public async Task AddDrive_EmptyInitializer_ShouldRewriteToWithDrive() + { + // `new MockDriveData()` with no initializer has nothing to chain — the + // rewrite collapses to the single-argument WithDrive overload. The using + // must also swap, since WithDrive is Testably-only. + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs) => {|#0:fs.AddDrive("D:", new MockDriveData())|}; + } + """; + + const string fixedSource = """ + using Testably.Abstractions.Testing; + + public class C + { + public void Run(MockFileSystem fs) => fs.WithDrive("D:"); + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + fixedSource); + } + + [Fact] + public async Task AddDrive_TotalSizeOnly_ShouldRewriteToWithDriveLambda() + { + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs) + => {|#0:fs.AddDrive("D:", new MockDriveData { TotalSize = 100 })|}; + } + """; + + const string fixedSource = """ + using Testably.Abstractions.Testing; + + public class C + { + public void Run(MockFileSystem fs) + => fs.WithDrive("D:", d => d.SetTotalSize(100)); + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + fixedSource); + } + + [Fact] + public async Task AddDrive_AllMappableProperties_ShouldChainSetters() + { + // Initializer order is preserved in the chained call so the user can verify + // any sequence-dependent behaviour by reading the rewrite top-down. + const string source = """ + using System.IO; + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs) + { + {|#0:fs.AddDrive("D:", new MockDriveData + { + TotalSize = 100, + IsReady = false, + DriveFormat = "NTFS", + DriveType = DriveType.Fixed, + })|}; + } + } + """; + + const string fixedSource = """ + using System.IO; + using Testably.Abstractions.Testing; + + public class C + { + public void Run(MockFileSystem fs) + { + fs.WithDrive("D:", d => d.SetTotalSize(100).SetIsReady(false).SetDriveFormat("NTFS").SetDriveType(DriveType.Fixed)); + } + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + fixedSource); + } + + [Fact] + public async Task AddDrive_TargetTypedNew_ShouldRewriteToWithDrive() + { + // `new() { ... }` resolves to MockDriveData via the AddDrive parameter type; + // the rewrite path treats it the same as an explicit `new MockDriveData()`. + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs) + => {|#0:fs.AddDrive("D:", new() { TotalSize = 200 })|}; + } + """; + + const string fixedSource = """ + using Testably.Abstractions.Testing; + + public class C + { + public void Run(MockFileSystem fs) + => fs.WithDrive("D:", d => d.SetTotalSize(200)); + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + fixedSource); + } + + [Fact] + public async Task AddDrive_ShadowingDriveIdentifier_PicksFreshLambdaParameter() + { + // The initializer RHS references an outer `d`; the rewrite must not let the + // lambda parameter shadow it. PickFreshDriveLambdaParameterName falls through + // to `drive` (next candidate). + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs, long d) + => {|#0:fs.AddDrive("D:", new MockDriveData { TotalSize = d })|}; + } + """; + + const string fixedSource = """ + using Testably.Abstractions.Testing; + + public class C + { + public void Run(MockFileSystem fs, long d) + => fs.WithDrive("D:", drive => drive.SetTotalSize(d)); + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + fixedSource); + } + + [Fact] + public async Task AddDrive_InterfaceTypedReceiver_HasNoFix() + { + // The rewrite emits `.WithDrive(...)`. IMockFileDataAccessor has no + // WithDrive, so the fix must not run when the user calls through the + // interface. + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(IMockFileDataAccessor accessor) + => {|#0:accessor.AddDrive("D:", new MockDriveData())|}; + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + source); + } + + [Fact] + public async Task AddDrive_NonLiteralMockDriveData_HasNoFix() + { + // A captured MockDriveData reference (parameter, local, field, etc.) has no + // safe textual rewrite — the user may pass a subclass or mutate the data. + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs, MockDriveData data) + => {|#0:fs.AddDrive("D:", data)|}; + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + source); + } + + [Fact] + public async Task AddDrive_UnsupportedInitializerProperty_HasNoFix() + { + // AvailableFreeSpace has no IStorageDrive setter — manual review required. + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs) + => {|#0:fs.AddDrive("D:", new MockDriveData { AvailableFreeSpace = 50 })|}; + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + source); + } + + [Fact] + public async Task AddDrive_CopyConstructor_HasNoFix() + { + // `new MockDriveData(template)` has no 1:1 mapping to a single WithDrive + // callback — the user might tweak fields after the copy. + const string source = """ + using System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(MockFileSystem fs, MockDriveData template) + => {|#0:fs.AddDrive("D:", new MockDriveData(template))|}; + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + source); + } + + [Fact] + public async Task AddDrive_FullyQualifiedReceiverDeclaration_HasNoFix() + { + // The parameter declaration uses the fully-qualified type name, so the + // using-swap can't retarget the receiver — after the swap, `fs` still binds + // to TestableIO MockFileSystem and `fs.WithDrive(...)` would not compile. + const string source = """ + public class C + { + public void Run(System.IO.Abstractions.TestingHelpers.MockFileSystem fs) + => {|#0:fs.AddDrive("D:", new System.IO.Abstractions.TestingHelpers.MockDriveData())|}; + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + source); + } + + [Fact] + public async Task AddDrive_AliasQualifiedReceiverDeclaration_HasNoFix() + { + // The parameter declaration is alias-qualified (`TestableIo.MockFileSystem`). + // The using-swap touches `using System.IO.Abstractions.TestingHelpers;` but + // leaves the alias `using TestableIo = ...;` in place, so `fs` stays bound to + // TestableIO and the rewrite would not compile. + const string source = """ + using TestableIo = System.IO.Abstractions.TestingHelpers; + + public class C + { + public void Run(TestableIo.MockFileSystem fs) + => {|#0:fs.AddDrive("D:", new TestableIo.MockDriveData())|}; + } + """; + + await Verifier.VerifyCodeFixAsync( + source, + Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0), + source); + } } }