feat: implement analyzer and code fixer for identifying and fixing when a sync flag is not propagated correctly.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
## Release 0.0
|
||||
|
||||
### New Rules
|
||||
|
||||
Rule ID | Category | Severity | Notes
|
||||
--------|--------------|----------|----------------------
|
||||
SYNC001 | Architecture | Error | SyncParameterAnalyzer
|
||||
@@ -0,0 +1,52 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageId>StevanFreeborn.AsyncSyncFlagAnalyzer</PackageId>
|
||||
<Version>0.0.0</Version>
|
||||
<Authors>Stevan Freeborn</Authors>
|
||||
<Description>Roslyn analyzer to enforce passing the sync flag in optionally asynchronous execution paths.</Description>
|
||||
<PackageTags>analyzer,async,sync,flag</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
|
||||
<DevelopmentDependency>true</DevelopmentDependency>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(GITEA_ACTIONS)' == 'true'">
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StevanFreeborn.AsyncSyncFlagAnalyzer.CodeFixes\StevanFreeborn.AsyncSyncFlagAnalyzer.CodeFixes.csproj" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="..\StevanFreeborn.AsyncSyncFlagAnalyzer.Common\StevanFreeborn.AsyncSyncFlagAnalyzer.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="IncludeCodeFixInPackage" BeforeTargets="GenerateNuspec" DependsOnTargets="Build">
|
||||
<ItemGroup>
|
||||
<_PackageFiles Include="$(MSBuildProjectDirectory)\..\$(MSBuildProjectName).CodeFixes\bin\$(Configuration)\$(TargetFramework)\$(MSBuildProjectName).CodeFixes.dll">
|
||||
<PackagePath>analyzers/dotnet/cs</PackagePath>
|
||||
<Visible>false</Visible>
|
||||
<BuildAction>None</BuildAction>
|
||||
</_PackageFiles>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,130 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
using StevanFreeborn.AsyncSyncFlagAnalyzer.Common;
|
||||
|
||||
namespace StevanFreeborn.AsyncSyncFlagAnalyzer;
|
||||
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class SyncParameterAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private const string Title = "Missing sync parameter in optionally async call";
|
||||
private const string MessageFormat = "The method '{0}' must pass the '{1}' parameter to '{2}'";
|
||||
|
||||
private static readonly DiagnosticDescriptor Rule = new(
|
||||
DiagnosticProperties.DiagnosticId,
|
||||
Title,
|
||||
MessageFormat,
|
||||
"Architecture",
|
||||
DiagnosticSeverity.Error,
|
||||
isEnabledByDefault: true
|
||||
);
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
context.EnableConcurrentExecution();
|
||||
context.RegisterSyntaxNodeAction(AnalyzeAwaitExpression, SyntaxKind.AwaitExpression);
|
||||
}
|
||||
|
||||
private void AnalyzeAwaitExpression(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var awaitExpr = (AwaitExpressionSyntax)context.Node;
|
||||
|
||||
var enclosingMethod = awaitExpr.FirstAncestorOrSelf<MethodDeclarationSyntax>();
|
||||
|
||||
if (enclosingMethod is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var validSyncNames = GetValidSyncNames(context);
|
||||
|
||||
var enclosingSyncParam = enclosingMethod.ParameterList.Parameters
|
||||
.FirstOrDefault(p => validSyncNames.Contains(p.Identifier.Text));
|
||||
|
||||
if (enclosingSyncParam is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var expectedSyncName = enclosingSyncParam.Identifier.Text;
|
||||
|
||||
if (awaitExpr.Expression is not InvocationExpressionSyntax invocation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (context.SemanticModel.GetSymbolInfo(invocation).Symbol is not IMethodSymbol methodSymbol)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetSyncParam = methodSymbol.Parameters.FirstOrDefault(p => validSyncNames.Contains(p.Name));
|
||||
|
||||
if (targetSyncParam is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var passedSync = false;
|
||||
|
||||
foreach (var argument in invocation.ArgumentList.Arguments)
|
||||
{
|
||||
if (argument.Expression is IdentifierNameSyntax id && id.Identifier.Text == expectedSyncName)
|
||||
{
|
||||
passedSync = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!passedSync)
|
||||
{
|
||||
var properties = ImmutableDictionary<string, string?>.Empty
|
||||
.Add(DiagnosticProperties.EnclosingSyncName, enclosingSyncParam.Identifier.Text)
|
||||
.Add(DiagnosticProperties.TargetSyncName, targetSyncParam.Name)
|
||||
.Add(DiagnosticProperties.TargetSyncOrdinal, targetSyncParam.Ordinal.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
var diagnostic = Diagnostic.Create(
|
||||
Rule,
|
||||
invocation.GetLocation(),
|
||||
properties,
|
||||
enclosingMethod.Identifier.Text,
|
||||
expectedSyncName,
|
||||
methodSymbol.Name
|
||||
);
|
||||
|
||||
context.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<string> GetValidSyncNames(SyntaxNodeAnalysisContext context)
|
||||
{
|
||||
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { DiagnosticProperties.DefaultSyncName };
|
||||
var options = context.Options.AnalyzerConfigOptionsProvider.GetOptions(context.Node.SyntaxTree);
|
||||
|
||||
if (options.TryGetValue(DiagnosticProperties.EditorConfigKey, out var customNames) && !string.IsNullOrWhiteSpace(customNames))
|
||||
{
|
||||
var splitNames = customNames.Split([','], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var name in splitNames)
|
||||
{
|
||||
_ = names.Add(name.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user