Skip to content
Draft
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
119 changes: 119 additions & 0 deletions src/TaskAnalyzer.Tests/MultiThreadableTaskAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1870,6 +1870,125 @@ public override bool Execute()
// Scope option tests
// ═══════════════════════════════════════════════════════════════════════

[Fact]
public async Task Scope_Default_PlainTask_DoesNotGetEnvironmentOrPathDiagnostics()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using System.IO;
public class PlainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
var value = Environment.GetEnvironmentVariable("KEY");
return File.Exists("relative.txt");
}
}
""");

diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldBeEmpty();
diags.Where(d => d.Id == DiagnosticIds.FilePathRequiresAbsolute).ShouldBeEmpty();
}

[Fact]
public async Task Scope_Default_MultiThreadableTask_GetsEnvironmentAndPathDiagnostics()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using System.IO;
using Microsoft.Build.Framework;
public class MtTask : Microsoft.Build.Utilities.Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
public override bool Execute()
{
var value = Environment.GetEnvironmentVariable("KEY");
return File.Exists("relative.txt");
}
}
""");

diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldHaveSingleItem();
diags.Where(d => d.Id == DiagnosticIds.FilePathRequiresAbsolute).ShouldHaveSingleItem();
}

[Fact]
public async Task Scope_All_PlainTask_GetsEnvironmentAndPathDiagnostics()
{
var diags = await GetDiagnosticsWithScopeAsync("""
using System;
using System.IO;
public class PlainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
var value = Environment.GetEnvironmentVariable("KEY");
return File.Exists("relative.txt");
}
}
""", SharedAnalyzerHelpers.ScopeAll);

diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldHaveSingleItem();
diags.Where(d => d.Id == DiagnosticIds.FilePathRequiresAbsolute).ShouldHaveSingleItem();
}

[Fact]
public async Task Scope_UnrecognizedValue_UsesDefault()
{
var diags = await GetDiagnosticsWithScopeAsync("""
using System;
public class PlainTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
var value = Environment.GetEnvironmentVariable("KEY");
return true;
}
}
""", "unrecognized");

diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldBeEmpty();
}

[Fact]
public async Task Scope_Default_MultiThreadableAttribute_OptsTaskIn()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using Microsoft.Build.Framework;
[MSBuildMultiThreadableTask]
public class MtTask : Microsoft.Build.Utilities.Task
{
public override bool Execute()
{
var value = Environment.GetEnvironmentVariable("KEY");
return true;
}
}
""");

diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldHaveSingleItem();
}

[Fact]
public async Task Scope_Default_AnalyzedAttribute_OptsHelperIn()
{
var diags = await GetDiagnosticsWithDefaultScopeAsync("""
using System;
using Microsoft.Build.Framework;
[MSBuildMultiThreadableTaskAnalyzed]
public class MtHelper
{
public void Execute()
{
var value = Environment.GetEnvironmentVariable("KEY");
}
}
""");

diags.Where(d => d.Id == DiagnosticIds.TaskEnvironmentRequired).ShouldHaveSingleItem();
}

[Fact]
public async Task Scope_MultithreadableOnly_PlainTask_NoDiagnostic()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ private static CSharpCodeFixTest<MultiThreadableTaskAnalyzer, MultiThreadableTas
DiagnosticIds.TaskEnvironmentRequired => new DiagnosticResult(DiagnosticDescriptors.TaskEnvironmentRequired),
DiagnosticIds.FilePathRequiresAbsolute => new DiagnosticResult(DiagnosticDescriptors.FilePathRequiresAbsolute),
DiagnosticIds.PotentialIssue => new DiagnosticResult(DiagnosticDescriptors.PotentialIssue),
DiagnosticIds.TransitiveUnsafeCall => new DiagnosticResult(DiagnosticDescriptors.TransitiveUnsafeCall),
_ => new DiagnosticResult(id, DiagnosticSeverity.Warning),
};

Expand Down
96 changes: 72 additions & 24 deletions src/TaskAnalyzer.Tests/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,34 +138,16 @@ public static string FullyQualifiedPath(string tail) =>
public static MetadataReference[] GetCoreReferences() => s_coreReferences;

/// <summary>
/// Runs the MultiThreadableTaskAnalyzer on the given source code and returns analyzer diagnostics.
/// Source is combined with framework stubs automatically.
/// Runs the MultiThreadableTaskAnalyzer in explicit all-task migration mode.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(string source)
{
var compilation = CreateCompilation(source);
var analyzer = new MultiThreadableTaskAnalyzer();
var compilationWithAnalyzers = compilation.WithAnalyzers(
ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));

var allDiags = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
return allDiags;
}
public static System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(string source) =>
GetDiagnosticsWithScopeAsync(source, SharedAnalyzerHelpers.ScopeAll);

/// <summary>
/// Runs BOTH the direct and transitive analyzers on the given source code.
/// Runs both the direct and transitive analyzers in explicit all-task migration mode.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsAsync(string source)
{
var compilation = CreateCompilation(source);
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(
new MultiThreadableTaskAnalyzer(),
new TransitiveCallChainAnalyzer());
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);

var allDiags = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
return allDiags;
}
public static System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsAsync(string source) =>
GetAllDiagnosticsWithScopeAsync(source, SharedAnalyzerHelpers.ScopeAll);

/// <summary>
/// Runs compiler diagnostics together with analyzers and suppressors and returns
Expand Down Expand Up @@ -260,6 +242,72 @@ public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetD
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

/// <summary>
/// Runs the MultiThreadableTaskAnalyzer without a scope option.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetDiagnosticsWithDefaultScopeAsync(string source)
{
var compilation = CreateCompilation(source);
var analyzer = new MultiThreadableTaskAnalyzer();
var compilationWithAnalyzers = compilation.WithAnalyzers(
ImmutableArray.Create<DiagnosticAnalyzer>(analyzer));
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

/// <summary>
/// Runs both the direct and transitive analyzers with a specific scope option.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsWithScopeAsync(string source, string scope)
{
var compilation = CreateCompilation(source);
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(
new MultiThreadableTaskAnalyzer(),
new TransitiveCallChainAnalyzer());

var globalOptions = new Dictionary<string, string>
{
{ $"build_property.{SharedAnalyzerHelpers.ScopeOptionKey}", scope }
};
var optionsProvider = new TestAnalyzerConfigOptionsProvider(globalOptions);
var options = new AnalyzerOptions(ImmutableArray<AdditionalText>.Empty, optionsProvider);

var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers, options);
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

/// <summary>
/// Runs both the direct and transitive analyzers without a scope option.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsWithDefaultScopeAsync(string source)
{
var compilation = CreateCompilation(source);
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(
new MultiThreadableTaskAnalyzer(),
new TransitiveCallChainAnalyzer());
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

/// <summary>
/// Runs both direct and transitive analyzers with a configured action for one diagnostic ID.
/// </summary>
public static async System.Threading.Tasks.Task<ImmutableArray<Diagnostic>> GetAllDiagnosticsWithDiagnosticActionAsync(
string source,
string diagnosticId,
ReportDiagnostic action)
{
var compilation = CreateCompilation(source);
var options = ((CSharpCompilationOptions)compilation.Options).WithSpecificDiagnosticOptions(
compilation.Options.SpecificDiagnosticOptions.SetItem(diagnosticId, action));
compilation = compilation.WithOptions(options);

var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(
new MultiThreadableTaskAnalyzer(),
new TransitiveCallChainAnalyzer());
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
return await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync();
}

private static MetadataReference[] CreateCoreReferences()
{
// Reference the core runtime assemblies needed
Expand Down
Loading