feat: implement analyzer and code fixer for identifying and fixing when a sync flag is not propagated correctly.

This commit is contained in:
Stevan Freeborn
2026-06-18 21:28:41 -05:00
parent 86b9673b24
commit f3da2f60fd
45 changed files with 2184 additions and 0 deletions
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<Compile Remove="TestData\**" />
<Content Include="TestData\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\StevanFreeborn.AsyncSyncFlagAnalyzer\StevanFreeborn.AsyncSyncFlagAnalyzer.csproj" />
<ProjectReference Include="..\..\src\StevanFreeborn.AsyncSyncFlagAnalyzer.CodeFixes\StevanFreeborn.AsyncSyncFlagAnalyzer.CodeFixes.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,159 @@
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using StevanFreeborn.AsyncSyncFlagAnalyzer.CodeFixes;
using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<
StevanFreeborn.AsyncSyncFlagAnalyzer.SyncParameterAnalyzer,
StevanFreeborn.AsyncSyncFlagAnalyzer.CodeFixes.SyncParameterCodeFixProvider,
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
namespace StevanFreeborn.AsyncSyncFlagAnalyzer.Tests;
public class SyncParameterAnalyzerTests
{
[Fact]
public async Task WhenSyncIsPassedCorrectly_ItShouldNotReportDiagnostic()
{
var testData = await LoadTestDataAsync("WhenSyncIsPassedCorrectly");
await VerifyCS.VerifyAnalyzerAsync(testData);
}
[Fact]
public async Task WhenSyncIsPassedPositionally_ItShouldNotReportDiagnostic()
{
var testData = await LoadTestDataAsync("WhenSyncIsPassedPositionally");
await VerifyCS.VerifyAnalyzerAsync(testData);
}
[Fact]
public async Task WhenSyncIsPassedAsNamed_ItShouldNotReportDiagnostic()
{
var testData = await LoadTestDataAsync("WhenSyncIsPassedAsNamed");
await VerifyCS.VerifyAnalyzerAsync(testData);
}
[Fact]
public async Task WhenSyncIsOmitted_ItShouldReportDiagnosticAndApplyCodeFix()
{
var expectedDiagnostic = VerifyCS.Diagnostic("SYNC001")
.WithLocation(0)
.WithArguments("GetFrobCoreAsync", "sync", "GetCoreAsync");
var testData = await LoadTestDataAsync("WhenSyncIsOmitted");
var fixedData = await LoadTestDataAsync("WhenSyncIsOmitted_Fixed");
await VerifyCS.VerifyCodeFixAsync(testData, expectedDiagnostic, fixedData);
}
[Fact]
public async Task WhenSyncIsHardcodedFalse_ItShouldReportDiagnostic()
{
var expectedDiagnostic = VerifyCS.Diagnostic("SYNC001")
.WithLocation(0)
.WithArguments("GetFrobCoreAsync", "sync", "GetCoreAsync");
var testData = await LoadTestDataAsync("WhenSyncIsHardcodedFalse");
await VerifyCS.VerifyAnalyzerAsync(testData, expectedDiagnostic);
}
[Fact]
public async Task WhenSyncIsHardcodedFalse_ItShouldApplyCodeFix()
{
var expectedDiagnostic = VerifyCS.Diagnostic("SYNC001")
.WithLocation(0)
.WithArguments("GetFrobCoreAsync", "sync", "GetCoreAsync");
var testData = await LoadTestDataAsync("WhenSyncIsHardcodedFalse");
var fixedData = await LoadTestDataAsync("WhenSyncIsHardcodedFalse_Fixed");
await VerifyCS.VerifyCodeFixAsync(testData, expectedDiagnostic, fixedData);
}
[Fact]
public async Task WhenSyncIsPassedAsNamedHardcoded_ItShouldReplaceNamedArgument()
{
var expectedDiagnostic = VerifyCS.Diagnostic("SYNC001")
.WithLocation(0)
.WithArguments("GetFrobCoreAsync", "sync", "GetCoreAsync");
var testData = await LoadTestDataAsync("WhenSyncIsPassedAsNamedHardcoded");
var fixedData = await LoadTestDataAsync("WhenSyncIsPassedAsNamedHardcoded_Fixed");
await VerifyCS.VerifyCodeFixAsync(testData, expectedDiagnostic, fixedData);
}
[Fact]
public async Task WhenOuterMethodHasNoSyncParam_ItShouldNotReportDiagnostic()
{
var testData = await LoadTestDataAsync("WhenOuterMethodHasNoSyncParam");
await VerifyCS.VerifyAnalyzerAsync(testData);
}
[Fact]
public async Task WhenTargetHasNoSyncParam_ItShouldNotReportDiagnostic()
{
var testData = await LoadTestDataAsync("WhenTargetHasNoSyncParam");
await VerifyCS.VerifyAnalyzerAsync(testData);
}
[Fact]
public async Task WhenSyncIsOmitted_CustomName_ItShouldReportDiagnosticAndApplyCodeFix()
{
var expectedDiagnostic = VerifyCS.Diagnostic("SYNC001")
.WithLocation(0)
.WithArguments("GetFrobCoreAsync", "runSynchronously", "GetMoreDataAsync");
var testData = await LoadTestDataAsync("WhenSyncIsOmitted_CustomName");
var fixedData = await LoadTestDataAsync("WhenSyncIsOmitted_CustomName_Fixed");
var test = new CSharpCodeFixTest<SyncParameterAnalyzer, SyncParameterCodeFixProvider, DefaultVerifier>
{
TestCode = testData,
FixedCode = fixedData
};
test.TestState.AnalyzerConfigFiles.Add(("/.editorconfig", """
root = true
[*.cs]
dotnet_diagnostic.SYNC001.additional_sync_names = runSynchronously
"""));
test.ExpectedDiagnostics.Add(expectedDiagnostic);
await test.RunAsync();
}
[Fact]
public async Task WhenSyncIsOmitted_MultipleAwaits_OnlyOneShouldReportDiagnostic()
{
var expectedDiagnostic = VerifyCS.Diagnostic("SYNC001")
.WithLocation(0)
.WithArguments("GetFrobCoreAsync", "sync", "GetMoreDataAsync");
var testData = await LoadTestDataAsync("WhenSyncIsOmitted_MultipleAwaits");
var fixedData = await LoadTestDataAsync("WhenSyncIsOmitted_MultipleAwaits_Fixed");
await VerifyCS.VerifyCodeFixAsync(testData, expectedDiagnostic, fixedData);
}
[Fact]
public async Task WhenSyncIsNotLastTargetParameter_ItShouldReportDiagnosticAndApplyCodeFix()
{
var expectedDiagnostic = VerifyCS.Diagnostic("SYNC001")
.WithLocation(0)
.WithArguments("GetFrobCoreAsync", "sync", "GetDataAsync");
var testData = await LoadTestDataAsync("WhenSyncIsNotLastTargetParameter");
var fixedData = await LoadTestDataAsync("WhenSyncIsNotLastTargetParameter_Fixed");
await VerifyCS.VerifyCodeFixAsync(testData, expectedDiagnostic, fixedData);
}
private static async Task<string> LoadTestDataAsync(string name)
{
var path = Path.Combine(AppContext.BaseDirectory, "TestData", $"{name}.cs");
var text = await File.ReadAllTextAsync(path);
return text;
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync()
{
return await _dataService.GetCoreAsync(17);
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await {|#0:_dataService.GetCoreAsync(17, false)|};
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await _dataService.GetCoreAsync(17, sync);
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetDataAsync(int id, bool sync = false, bool useCache = true);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await {|#0:_dataService.GetDataAsync(17)|};
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetDataAsync(int id, bool sync = false, bool useCache = true);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await _dataService.GetDataAsync(17, sync);
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await {|#0:_dataService.GetCoreAsync(17)|};
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetMoreDataAsync(bool runSynchronously = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool runSynchronously)
{
return await {|#0:_dataService.GetMoreDataAsync()|};
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetMoreDataAsync(bool runSynchronously = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool runSynchronously)
{
return await _dataService.GetMoreDataAsync(runSynchronously);
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await _dataService.GetCoreAsync(17, sync);
}
}
@@ -0,0 +1,20 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetDataAsync(bool sync = false);
Task<string> GetMoreDataAsync(bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
var a = await _dataService.GetDataAsync(sync);
var b = await {|#0:_dataService.GetMoreDataAsync()|};
return a + b;
}
}
@@ -0,0 +1,20 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetDataAsync(bool sync = false);
Task<string> GetMoreDataAsync(bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
var a = await _dataService.GetDataAsync(sync);
var b = await _dataService.GetMoreDataAsync(sync);
return a + b;
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await _dataService.GetCoreAsync(id: 17, sync: sync);
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await {|#0:_dataService.GetCoreAsync(17, sync: false)|};
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await _dataService.GetCoreAsync(17, sync: sync);
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await _dataService.GetCoreAsync(17, sync);
}
}
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
public interface IDataService
{
Task<string> GetCoreAsync(int id, bool sync = false);
}
public class BusinessLogic
{
private readonly IDataService _dataService;
public BusinessLogic(IDataService dataService) { _dataService = dataService; }
private async Task<string> GetFrobCoreAsync(bool sync)
{
return await _dataService.GetCoreAsync(17, sync);
}
}
@@ -0,0 +1,10 @@
using System.Threading.Tasks;
public class BusinessLogic
{
private async Task<string> GetFrobCoreAsync(bool sync)
{
await Task.Delay(100);
return "done";
}
}