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
15 changes: 15 additions & 0 deletions Source/Testably.Abstractions.Migration.Analyzers/Patterns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,19 @@ public static class Patterns

/// <summary><c>new MockFileData(MockFileData template)</c> — clone semantics differ; no Testably equivalent.</summary>
public const string MockFileDataCopyConstructor = "MockFileData.copyCtor";

/// <summary>
/// A read access to a <c>MockFileData</c> property whose receiver is a captured
/// reference (local, parameter, field, etc.) rather than a one-shot
/// <c>fs.GetFile(path)</c> invocation — no safe textual rewrite without flow
/// analysis.
/// </summary>
public const string MockFileDataCapturedReferenceRead = "MockFileData.capturedReferenceRead";

/// <summary>
/// A write/assignment to a <c>MockFileData</c> property whose receiver is a
/// captured reference rather than a one-shot <c>fs.GetFile(path)</c> invocation
/// — no safe textual rewrite without flow analysis.
Comment on lines +94 to +104
/// </summary>
public const string MockFileDataCapturedReferenceWrite = "MockFileData.capturedReferenceWrite";
}
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,22 @@
return;
}

string pattern = isWrite
? Patterns.MockFileDataPropertyWrite
: Patterns.MockFileDataPropertyRead;
// Phase 4c: separate captured-reference accesses from one-shot
// `fs.GetFile(path).Prop` chains. The code-fix's rewrite only applies to the
// one-shot shape; everything else needs flow analysis to retarget safely, so
// give it a discriminating pattern id and let the fix dispatch fall through.
bool isOneShotGetFile = symbols.MockFileSystem is not null
&& propertyRef.Instance is IInvocationOperation invocation
&& invocation.TargetMethod.Name == "GetFile"
&& SymbolEqualityComparer.Default.Equals(
invocation.TargetMethod.ContainingType, symbols.MockFileSystem)
&& invocation.TargetMethod.Parameters.Length == 1
&& invocation.TargetMethod.Parameters[0].Type.SpecialType == SpecialType.System_String
&& invocation.Arguments.Length == 1;

string pattern = isOneShotGetFile
? (isWrite ? Patterns.MockFileDataPropertyWrite : Patterns.MockFileDataPropertyRead)

Check warning on line 234 in Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=Testably_Testably.Abstractions.Migration&issues=AZ4qXQbDC2QocoKmEkeV&open=AZ4qXQbDC2QocoKmEkeV&pullRequest=12
: (isWrite ? Patterns.MockFileDataCapturedReferenceWrite : Patterns.MockFileDataCapturedReferenceRead);

Check warning on line 235 in Source/Testably.Abstractions.Migration.Analyzers/SystemIOAbstractionsAnalyzer.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

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

Report(context, propertyRef.Syntax.GetLocation(), pattern);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,21 @@

await That(fs.File.ReadAllText("/a")).IsEqualTo("world");
}

[Fact]
public async Task CapturedReference_ReadsAndWritesViaLocal()
{
// Phase 4c manual-review fixture: GetFile result is captured into a local before
// any property access, so the analyzer flags each access as a captured reference
// rather than a migratable one-shot chain.
MockFileSystem fs = new();
fs.AddFile("/a", new MockFileData("hello") { Attributes = FileAttributes.Normal });

Check warning on line 104 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/MockFileDataTests.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=AZ4qXQYkC2QocoKmEkeS&open=AZ4qXQYkC2QocoKmEkeS&pullRequest=12
MockFileData data = fs.GetFile("/a");

string before = data.TextContents;

Check warning on line 107 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/MockFileDataTests.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=AZ4qXQYkC2QocoKmEkeT&open=AZ4qXQYkC2QocoKmEkeT&pullRequest=12
data.Attributes = FileAttributes.ReadOnly;

Check warning on line 108 in Tests/Testably.Abstractions.Migration.SystemIOAbstractionsPlayground/MockFileDataTests.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=AZ4qXQYkC2QocoKmEkeU&open=AZ4qXQYkC2QocoKmEkeU&pullRequest=12

await That(before).IsEqualTo("hello");
await That(fs.File.GetAttributes("/a")).IsEqualTo(FileAttributes.ReadOnly);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,85 @@ await Verifier.VerifyAnalyzerAsync(
Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0));
}

[Fact]
public async Task MockFileDataCapturedReferenceRead_FromLocal_ShouldBeFlagged()
{
const string source = """
using System.IO.Abstractions.TestingHelpers;

public class C
{
public string Read(MockFileSystem fs)
{
MockFileData data = fs.GetFile("/a");
return {|#0:data.TextContents|};
}
}
""";

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

[Fact]
public async Task MockFileDataCapturedReferenceWrite_FromLocal_ShouldBeFlagged()
{
const string source = """
using System.IO;
using System.IO.Abstractions.TestingHelpers;

public class C
{
public void Write(MockFileSystem fs)
{
MockFileData data = fs.GetFile("/a");
{|#0:data.Attributes|} = FileAttributes.ReadOnly;
}
}
""";

await Verifier.VerifyAnalyzerAsync(
source,
Verifier.Diagnostic(Rules.SystemIOAbstractionsRule).WithLocation(0));
Comment on lines +322 to +362
}

[Fact]
public async Task MockFileDataCapturedReferenceRead_FromParameter_ShouldBeFlagged()
{
const string source = """
using System.IO.Abstractions.TestingHelpers;

public class C
{
public string Read(MockFileData data) => {|#0:data.TextContents|};
}
""";

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

[Fact]
public async Task MockFileDataCapturedReferenceRead_FromField_ShouldBeFlagged()
{
const string source = """
using System.IO.Abstractions.TestingHelpers;

public class C
{
private MockFileData data = new("hello");

public string Read() => {|#0:data.TextContents|};
}
""";

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

[Fact]
public async Task WithoutTestingHelpersAssembly_ShouldDoNothing()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ await Verifier.VerifyCodeFixAsync(
public async Task MockFileDataRead_CapturedReference_HasNoFix()
{
// `data` is a captured MockFileData reference, not a one-shot `GetFile(p).Prop`.
// Without flow analysis we cannot safely rewrite. Phase 4 will surface this case.
// The analyzer (Phase 4c) classifies this as MockFileDataCapturedReferenceRead
// so the code-fix dispatcher falls through with no rewrite — flow analysis is
// required to retarget safely.
const string source = """
using System.IO.Abstractions.TestingHelpers;

Expand Down
Loading