Skip to content
Open
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
134 changes: 134 additions & 0 deletions src/TaskAnalyzer.Tests/MultiThreadableTaskAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,140 @@ public override bool Execute()
// Safe wrapper recognition: Path.GetDirectoryName, Path.Combine, Path.GetFullPath
// ═══════════════════════════════════════════════════════════════════════

[Theory]
[InlineData("Path.GetDirectoryName(TargetFile)")]
[InlineData("Path.GetPathRoot(TargetFile)")]
[InlineData("System.IO.Path.GetDirectoryName(path: TargetFile)")]
[InlineData("IOPath.GetPathRoot(TargetFile)")]
[InlineData("(GetDirectoryName(TargetFile))!")]
public async Task InvertedPathExtraction_ProducesDiagnostic(string expression)
{
var source = $$"""
using System.IO;
using IOPath = System.IO.Path;
using static System.IO.Path;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public string TargetFile { get; set; } = "list.xml";
public override bool Execute()
{
Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath({{expression}}));
return true;
}
}
""";
var diags = await GetDiagnosticsAsync(source);

var diagnostic = diags.ShouldHaveSingleItem();
diagnostic.Id.ShouldBe(DiagnosticIds.ResolvePathBeforeExtraction);
source.Substring(diagnostic.Location.SourceSpan.Start, diagnostic.Location.SourceSpan.Length)
.ShouldBe($"TaskEnvironment.GetAbsolutePath({expression})");
}

[Fact]
public async Task InvertedPathExtraction_SystemIOPathLookalike_NoDiagnostic()
{
var diags = await GetDiagnosticsAsync("""
using Microsoft.Build.Framework;
namespace System.IO
{
internal static class Path
{
public static string GetDirectoryName(string path) => path;
}
}
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
var dir = TaskEnvironment.GetAbsolutePath(System.IO.Path.GetDirectoryName("list.xml"));
return true;
}
}
""");

diags.ShouldNotContain(d => d.Id == DiagnosticIds.ResolvePathBeforeExtraction);
}

[Theory]
[InlineData("GetDirectoryName")]
[InlineData("GetPathRoot")]
public async Task PathExtraction_AfterResolvingPath_NoDiagnostic(string method)
{
var diags = await GetDiagnosticsAsync($$"""
using System.IO;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
string dir = Path.{{method}}(TaskEnvironment.GetAbsolutePath("list.xml"));
Directory.CreateDirectory(dir);
return true;
}
}
""");

diags.ShouldBeEmpty();
}

[Theory]
[InlineData("all", "", 1)]
[InlineData("multithreadable_only", "", 0)]
[InlineData("multithreadable_only", ", IMultiThreadableTask", 1)]
public async Task InvertedPathExtraction_RespectsScope(string scope, string taskInterface, int expectedCount)
{
var diags = await GetDiagnosticsWithScopeAsync($$"""
using System.IO;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task{{taskInterface}}
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
var dir = TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName("list.xml"));
return true;
}
}
""", scope);

diags.Length.ShouldBe(expectedCount);
diags.ShouldAllBe(d => d.Id == DiagnosticIds.ResolvePathBeforeExtraction);
}

[Fact]
public async Task PathExtraction_UnrelatedMethods_NoDiagnostic()
{
var diags = await GetDiagnosticsAsync("""
using System.IO;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
var a = TaskEnvironment.GetAbsolutePath(OtherPath.GetDirectoryName("list.xml"));
var b = TaskEnvironment.GetAbsolutePath(OtherPath.GetPathRoot("list.xml"));
var c = GetAbsolutePath(Path.GetDirectoryName("list.xml"));
var d = TaskEnvironment.GetAbsolutePath(Path.GetFileName("dir/list.xml"));
return true;
}
private static string GetAbsolutePath(string path) => path;
}
public static class OtherPath
{
public static string GetDirectoryName(string path) => path;
public static string GetPathRoot(string path) => path;
}
""");

diags.ShouldBeEmpty();
}

[Fact]
public async Task DirectoryCreate_WithGetDirectoryNameOfAbsolutePath_NoDiagnostic()
{
Expand Down
201 changes: 201 additions & 0 deletions src/TaskAnalyzer.Tests/MultiThreadableTaskCodeFixProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
Expand Down Expand Up @@ -43,6 +44,7 @@ private static CSharpCodeFixTest<MultiThreadableTaskAnalyzer, MultiThreadableTas
DiagnosticIds.FilePathRequiresAbsolute => new DiagnosticResult(DiagnosticDescriptors.FilePathRequiresAbsolute),
DiagnosticIds.PotentialIssue => new DiagnosticResult(DiagnosticDescriptors.PotentialIssue),
DiagnosticIds.TransitiveUnsafeCall => new DiagnosticResult(DiagnosticDescriptors.TransitiveUnsafeCall),
DiagnosticIds.ResolvePathBeforeExtraction => new DiagnosticResult(DiagnosticDescriptors.ResolvePathBeforeExtraction),
_ => new DiagnosticResult(id, DiagnosticSeverity.Warning),
};

Expand Down Expand Up @@ -216,6 +218,205 @@ public override bool Execute()
.WithArguments("File.Exists(string?)", "wrap path argument with TaskEnvironment.GetAbsolutePath()")).RunAsync();
}

[Theory]
[InlineData("Path.GetDirectoryName(TargetFile)", "Path.GetDirectoryName(TaskEnvironment.GetAbsolutePath(TargetFile))")]
[InlineData("Path.GetPathRoot(TargetFile)", "Path.GetPathRoot(TaskEnvironment.GetAbsolutePath(TargetFile))")]
[InlineData("System.IO.Path.GetDirectoryName(TargetFile)", "System.IO.Path.GetDirectoryName(TaskEnvironment.GetAbsolutePath(TargetFile))")]
[InlineData("IOPath.GetPathRoot(TargetFile)", "IOPath.GetPathRoot(TaskEnvironment.GetAbsolutePath(TargetFile))")]
[InlineData("GetDirectoryName(TargetFile)", "GetDirectoryName(TaskEnvironment.GetAbsolutePath(TargetFile))")]
[InlineData("(Path.GetDirectoryName(path: TargetFile))!", "(Path.GetDirectoryName(path: TaskEnvironment.GetAbsolutePath(TargetFile)))!")]
[InlineData("Path.GetDirectoryName(/* file */ TargetFile)", "Path.GetDirectoryName(/* file */ TaskEnvironment.GetAbsolutePath(TargetFile))")]
[InlineData("Path.GetDirectoryName(Path.GetDirectoryName(TargetFile))", "Path.GetDirectoryName(Path.GetDirectoryName(TaskEnvironment.GetAbsolutePath(TargetFile)))")]
public async Task Fix_PathExtraction_WrapsOriginalPath(string expression, string fixedExpression)
{
var source = """
using System.IO;
using IOPath = System.IO.Path;
using static System.IO.Path;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public string TargetFile { get; set; } = "list.xml";
public override bool Execute()
{
REPLACE;
return true;
}
}
""";

await CreateFixTest(
source.Replace("REPLACE", "{|#0:Directory.CreateDirectory(path: " + expression + ")|}"),
source.Replace("REPLACE", "Directory.CreateDirectory(path: " + fixedExpression + ")"),
Diag(DiagnosticIds.FilePathRequiresAbsolute).WithLocation(0)
.WithArguments("Directory.CreateDirectory(string)", "wrap path argument with TaskEnvironment.GetAbsolutePath()")).RunAsync();
}

[Theory]
[InlineData("Path.GetDirectoryName(TargetFile)", "Path.GetDirectoryName(this.TaskEnvironment.GetAbsolutePath(path: TargetFile))", "GetDirectoryName")]
[InlineData("IOPath.GetPathRoot(path: TargetFile)", "IOPath.GetPathRoot(path: this.TaskEnvironment.GetAbsolutePath(path: TargetFile))", "GetPathRoot")]
[InlineData("(GetDirectoryName(TargetFile))!", "(GetDirectoryName(this.TaskEnvironment.GetAbsolutePath(path: TargetFile)))!", "GetDirectoryName")]
[InlineData("Path.GetDirectoryName(/* file */ TargetFile)", "Path.GetDirectoryName(/* file */ this.TaskEnvironment.GetAbsolutePath(path: TargetFile))", "GetDirectoryName")]
[InlineData("Path.GetDirectoryName(Path.GetDirectoryName(TargetFile))", "Path.GetDirectoryName(Path.GetDirectoryName(this.TaskEnvironment.GetAbsolutePath(path: TargetFile)))", "GetDirectoryName")]
public async Task Fix_InvertedPathExtraction_SwapsCalls(string expression, string fixedExpression, string method)
{
var source = """
using System.IO;
using IOPath = System.IO.Path;
using static System.IO.Path;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public string TargetFile { get; set; } = "list.xml";
public override bool Execute()
{
Directory.CreateDirectory(REPLACE);
return true;
}
}
""";

await CreateFixTest(
source.Replace("REPLACE", "{|#0:this.TaskEnvironment.GetAbsolutePath(path: " + expression + ")|}"),
source.Replace("REPLACE", fixedExpression),
Diag(DiagnosticIds.ResolvePathBeforeExtraction).WithLocation(0).WithArguments(method)).RunAsync();
}

[Theory]
[InlineData("GetDirectoryName")]
[InlineData("GetPathRoot")]
public async Task Fix_PathExtraction_LookalikeMethod_WrapsWholeExpression(string method)
{
var source = $$"""
using System.IO;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
REPLACE;
return true;
}
private static class Path
{
public static string {{method}}(string path) => path;
}
}
""";
var expression = $"Path.{method}(\"list.xml\")";

await CreateFixTest(
source.Replace("REPLACE", "{|#0:Directory.CreateDirectory(" + expression + ")|}"),
source.Replace("REPLACE", "Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(" + expression + "))"),
Diag(DiagnosticIds.FilePathRequiresAbsolute).WithLocation(0)
.WithArguments("Directory.CreateDirectory(string)", "wrap path argument with TaskEnvironment.GetAbsolutePath()")).RunAsync();
}

[Fact]
public async Task Fix_InvertedPathExtraction_AbsolutePathConsumers_NoFixOffered()
{
await CreateNoFixTest(
"""
using System.IO;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
AbsolutePath dir = {|#0:TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName("list.xml"))|};
var root = {|#1:TaskEnvironment.GetAbsolutePath(Path.GetPathRoot("list.xml"))|};
string value = root.Value;
return true;
}
}
""",
Diag(DiagnosticIds.ResolvePathBeforeExtraction).WithLocation(0).WithArguments("GetDirectoryName"),
Diag(DiagnosticIds.ResolvePathBeforeExtraction).WithLocation(1).WithArguments("GetPathRoot"));
}

[Fact]
public async Task Fix_InvertedPathExtraction_FixAllPreservesReceiver()
{
var source = """
using System.IO;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task
{
public override bool Execute() => true;
private static string Resolve(TaskEnvironment environment, string file)
{
string dir = DIRECTORY;
return ROOT;
}
}
""";
await CreateFixTest(
source.Replace("DIRECTORY", "{|#0:environment.GetAbsolutePath(Path.GetDirectoryName(file))|}")
.Replace("ROOT", "{|#1:environment.GetAbsolutePath(Path.GetPathRoot(file))|}"),
source.Replace("DIRECTORY", "Path.GetDirectoryName(environment.GetAbsolutePath(file))")
.Replace("ROOT", "Path.GetPathRoot(environment.GetAbsolutePath(file))"),
Diag(DiagnosticIds.ResolvePathBeforeExtraction).WithLocation(0).WithArguments("GetDirectoryName"),
Diag(DiagnosticIds.ResolvePathBeforeExtraction).WithLocation(1).WithArguments("GetPathRoot")).RunAsync();
}

[Theory]
[InlineData("GetDirectoryName", false, false)]
[InlineData("GetDirectoryName", true, false)]
[InlineData("GetPathRoot", false, false)]
[InlineData("GetPathRoot", true, false)]
[InlineData("GetDirectoryName", false, true)]
[InlineData("GetDirectoryName", true, true)]
[InlineData("GetPathRoot", false, true)]
[InlineData("GetPathRoot", true, true)]
public async Task Fix_PathExtraction_NullableInput(string method, bool inverted, bool suppressInput)
{
var source = """
using System.IO;
using Microsoft.Build.Framework;
public class MyTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; } = null!;
public string? TargetFile { get; set; }
public override bool Execute()
{
REPLACE;
return true;
}
}
""";
var input = suppressInput ? "TargetFile!" : "TargetFile";
var extraction = $"Path.{method}({input})!";
var original = inverted
? "Directory.CreateDirectory({|#0:TaskEnvironment.GetAbsolutePath(" + extraction + ")|})"
: "{|#0:Directory.CreateDirectory(" + extraction + ")|}";
var diagnostic = inverted
? Diag(DiagnosticIds.ResolvePathBeforeExtraction).WithLocation(0).WithArguments(method)
: Diag(DiagnosticIds.FilePathRequiresAbsolute).WithLocation(0)
.WithArguments("Directory.CreateDirectory(string)", "wrap path argument with TaskEnvironment.GetAbsolutePath()");
var fixedStatement = suppressInput
? $"Directory.CreateDirectory(Path.{method}(TaskEnvironment.GetAbsolutePath({input}))!)"
: original;
var test = CreateFixTest(source.Replace("REPLACE", original), source.Replace("REPLACE", fixedStatement), diagnostic);
if (!suppressInput)
{
test.FixedState.ExpectedDiagnostics.Add(diagnostic);
}

test.CompilerDiagnostics = CompilerDiagnostics.Warnings;
test.SolutionTransforms.Add((solution, projectId) =>
{
var project = solution.GetProject(projectId)!;
return solution.WithProjectCompilationOptions(projectId,
((CSharpCompilationOptions)project.CompilationOptions!).WithNullableContextOptions(NullableContextOptions.Enable))
.WithProjectParseOptions(projectId, project.ParseOptions!.WithDocumentationMode(DocumentationMode.Parse));
});
await test.RunAsync();
}

[Fact]
public async Task Fix_NewFileInfo_WrapsWithGetAbsolutePath()
{
Expand Down
1 change: 1 addition & 0 deletions src/TaskAnalyzer/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ MSBuildTask0011 | MSBuild.TaskAuthoring | Info | Prefer constructor injection fo
MSBuildTask0012 | MSBuild.TaskAuthoring | Warning | TaskEnvironment property is never assigned by MSBuild because the task does not implement IMultiThreadableTask
MSBuildTask0013 | MSBuild.TaskAuthoring | Info | Task declares IMultiThreadableTask but is not marked with [MSBuildMultiThreadableTask] (disabled by default)
MSBuildTask0014 | MSBuild.TaskAuthoring | Warning | [MSBuildMultiThreadableTask] applied to a type MSBuild never routes as a task -- not an ITask, or an abstract task whose attribute no subclass inherits -- where it has no effect
MSBuildTask0015 | MSBuild.TaskAuthoring | Warning | Resolve the original path before extracting its directory or root (code fix available)
Loading
Loading