Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e02e55e
Add Roslyn analyzers to detect incorrect usage of BenchmarkDotNet
silkfire Sep 26, 2025
a15e0ec
Unify C# language version
silkfire Oct 10, 2025
e1822c1
Remove Analyzers package projects
silkfire Oct 10, 2025
3eab33d
Revert BenchmarkDotNet.Disassembler changes
silkfire Oct 10, 2025
6aa546f
Reference Analyzers project from Annotations
silkfire Oct 10, 2025
bafb873
Move Benchmark.Analyzers and Benchmark.Analyzers.Tests to correct dir…
silkfire Oct 10, 2025
067574e
Remove accidentally added package Microsoft.CodeAnalysis.NetAnalyzers…
silkfire Oct 11, 2025
0416c10
* Benchmark classes annotated with a [GenericTypeArguments] attribute…
silkfire Oct 12, 2025
9bdda82
* Change diagnostic ID increment ordering
silkfire Oct 13, 2025
cbf5d88
When determining whether a class has any benchmark methods, iterate t…
silkfire Oct 13, 2025
957fc56
Move "Benchmark class cannot be sealed" to Run analyzer
silkfire Oct 13, 2025
d7c5e48
Move "Benchmark class must be public" to Run analyzer
silkfire Oct 13, 2025
a1b6e45
Support analyzing overload of BenchmarkRunner.Run that takes a Type p…
silkfire Oct 15, 2025
7d3dfc0
Remove requirement that a class must have at least one method annotat…
silkfire Oct 15, 2025
947e3bd
* Integer attribute values that fit within target type range should n…
silkfire Oct 16, 2025
2869a55
Use a dummy syntax tree to test whether types are implicitly convertible
silkfire Oct 17, 2025
9b995b6
Move "Generic class must be abstract or annotated with a [GenericType…
silkfire Oct 17, 2025
c178059
Add support to analyze implicit conversion from an array to a Span of…
silkfire Oct 17, 2025
41942fa
Add support to analyze implicit conversion when using constant values…
silkfire Oct 20, 2025
f6b41a9
* Add support to analyze a boolean constant value for the Baseline pr…
silkfire Oct 27, 2025
18e22df
Add test to OnlyOneMethodCanBeBaselinePerCategory rule verifying that…
silkfire Oct 27, 2025
7109692
Disable warnings for "Missing XML comment for publicly visible type o…
silkfire Oct 27, 2025
f48473c
Correct resource strings for OnlyOneMethodCanBeBaselinePerCategory rule
silkfire Oct 27, 2025
78a8cc1
Build error fixes
silkfire Oct 28, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ src/BenchmarkDotNet/Disassemblers/BenchmarkDotNet.Disassembler.*.nupkg
# Visual Studio 2015 cache/options directory
.vs/

# VSCode directory
.vscode/

# Cake
tools/**
.dotnet
Expand Down
43 changes: 43 additions & 0 deletions BenchmarkDotNet.Analyzers.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31710.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNet", "src\BenchmarkDotNet\BenchmarkDotNet.csproj", "{B5F58AA0-88F8-4C8C-B734-E1217E23079E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkDotNet.Annotations", "src\BenchmarkDotNet.Annotations\BenchmarkDotNet.Annotations.csproj", "{F07A7F74-15B6-4DC6-8617-A3A9C11C71EF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Analyzers", "src\BenchmarkDotNet.Analyzers\BenchmarkDotNet.Analyzers.csproj", "{AA4DDCA0-C1D8-ADA8-69FE-2F67C4CA96B1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Analyzers.Tests", "tests\BenchmarkDotNet.Analyzers.Tests\BenchmarkDotNet.Analyzers.Tests.csproj", "{7DE89F16-2160-42E3-004E-1F5064732121}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B5F58AA0-88F8-4C8C-B734-E1217E23079E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5F58AA0-88F8-4C8C-B734-E1217E23079E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5F58AA0-88F8-4C8C-B734-E1217E23079E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5F58AA0-88F8-4C8C-B734-E1217E23079E}.Release|Any CPU.Build.0 = Release|Any CPU
{F07A7F74-15B6-4DC6-8617-A3A9C11C71EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F07A7F74-15B6-4DC6-8617-A3A9C11C71EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F07A7F74-15B6-4DC6-8617-A3A9C11C71EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F07A7F74-15B6-4DC6-8617-A3A9C11C71EF}.Release|Any CPU.Build.0 = Release|Any CPU
{AA4DDCA0-C1D8-ADA8-69FE-2F67C4CA96B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA4DDCA0-C1D8-ADA8-69FE-2F67C4CA96B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA4DDCA0-C1D8-ADA8-69FE-2F67C4CA96B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA4DDCA0-C1D8-ADA8-69FE-2F67C4CA96B1}.Release|Any CPU.Build.0 = Release|Any CPU
{7DE89F16-2160-42E3-004E-1F5064732121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7DE89F16-2160-42E3-004E-1F5064732121}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DE89F16-2160-42E3-004E-1F5064732121}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DE89F16-2160-42E3-004E-1F5064732121}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {27411BE6-6445-400B-AB04-29B993B39CFF}
EndGlobalSection
EndGlobal
39 changes: 21 additions & 18 deletions NuGet.Config
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />

<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<!-- reuquired to run Mono AOT benchmarks -->
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
<add key="dotnet7" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json" />
<add key="dotnet8" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json" />
<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
<add key="dotnet10" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json" />
</packageSources>
</configuration>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />

<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<!-- required to run Mono AOT benchmarks -->
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
<add key="dotnet7" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json" />
<add key="dotnet8" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json" />
<add key="dotnet9" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
<add key="dotnet10" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json" />

<!-- required for Roslyn analyzers -->
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
</packageSources>
</configuration>
2 changes: 1 addition & 1 deletion build/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodingStyle.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>

<WarningsNotAsErrors>NU1900</WarningsNotAsErrors>
<Nullable>annotations</Nullable>
<!-- Suppress warning for nuget package used in old (unsupported) tfm. -->
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
Expand Down
243 changes: 243 additions & 0 deletions src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
namespace BenchmarkDotNet.Analyzers
{
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;

internal static class AnalyzerHelper
{
public static LocalizableResourceString GetResourceString(string name) => new(name, BenchmarkDotNetAnalyzerResources.ResourceManager, typeof(BenchmarkDotNetAnalyzerResources));

public static INamedTypeSymbol? GetBenchmarkAttributeTypeSymbol(Compilation compilation) => compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.BenchmarkAttribute");

public static bool AttributeListsContainAttribute(string attributeName, Compilation compilation, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel) => AttributeListsContainAttribute(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);

public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTypeSymbol, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
{
if (attributeTypeSymbol == null || attributeTypeSymbol.TypeKind == TypeKind.Error)
{
return false;
}

foreach (var attributeListSyntax in attributeLists)
{
foreach (var attributeSyntax in attributeListSyntax.Attributes)
{
var attributeSyntaxTypeSymbol = semanticModel.GetTypeInfo(attributeSyntax).Type;
if (attributeSyntaxTypeSymbol == null)
{
continue;
}

if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
{
return true;
}
}
}

return false;
}

public static bool AttributeListContainsAttribute(string attributeName, Compilation compilation, ImmutableArray<AttributeData> attributeList) => AttributeListContainsAttribute(compilation.GetTypeByMetadataName(attributeName), attributeList);

public static bool AttributeListContainsAttribute(INamedTypeSymbol? attributeTypeSymbol, ImmutableArray<AttributeData> attributeList)
{
if (attributeTypeSymbol == null || attributeTypeSymbol.TypeKind == TypeKind.Error)
{
return false;
}

return attributeList.Any(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default));
}

public static ImmutableArray<AttributeSyntax> GetAttributes(string attributeName, Compilation compilation, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel) => GetAttributes(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);

public static ImmutableArray<AttributeSyntax> GetAttributes(INamedTypeSymbol? attributeTypeSymbol, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
{
var attributesBuilder = ImmutableArray.CreateBuilder<AttributeSyntax>();

if (attributeTypeSymbol == null)
{
return attributesBuilder.ToImmutable();
}

foreach (var attributeListSyntax in attributeLists)
{
foreach (var attributeSyntax in attributeListSyntax.Attributes)
{
var attributeSyntaxTypeSymbol = semanticModel.GetTypeInfo(attributeSyntax).Type;
if (attributeSyntaxTypeSymbol == null)
{
continue;
}

if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
{
attributesBuilder.Add(attributeSyntax);
}
}
}

return attributesBuilder.ToImmutable();
}

public static int GetAttributeUsageCount(string attributeName, Compilation compilation, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel) => GetAttributeUsageCount(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);

public static int GetAttributeUsageCount(INamedTypeSymbol? attributeTypeSymbol, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
{
var attributeUsageCount = 0;

if (attributeTypeSymbol == null)
{
return 0;
}

foreach (var attributeListSyntax in attributeLists)
{
foreach (var attributeSyntax in attributeListSyntax.Attributes)
{
var attributeSyntaxTypeSymbol = semanticModel.GetTypeInfo(attributeSyntax).Type;
if (attributeSyntaxTypeSymbol == null)
{
continue;
}

if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
{
attributeUsageCount++;
}
}
}

return attributeUsageCount;
}

public static string NormalizeTypeName(INamedTypeSymbol namedTypeSymbol)
{
string typeName;

if (namedTypeSymbol.SpecialType != SpecialType.None)
{
typeName = namedTypeSymbol.ToString();
}
else if (namedTypeSymbol.IsUnboundGenericType)
{
typeName = $"{namedTypeSymbol.Name}<{new string(',', namedTypeSymbol.TypeArguments.Length - 1)}>";
}
else
{
typeName = namedTypeSymbol.Name;
}

return typeName;
}

public static bool IsAssignableToField(Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
{
const string codeTemplate1 = """
file static class Internal {{
static readonly {0} x = {1};
}}
""";

const string codeTemplate2 = """
file static class Internal {{
static readonly {0} x = ({1}){2};
}}
""";

return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, targetType, valueExpression, constantValue, valueType);
}

public static bool IsAssignableToLocal(Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
{
const string codeTemplate1 = """
file static class Internal {{
static void Method() {{
{0} x = {1};
}}
}}
""";

const string codeTemplate2 = """
file static class Internal {{
static void Method() {{
{0} x = ({1}){2};
}}
}}
""";

return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, targetType, valueExpression, constantValue, valueType);
}

private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
{
var hasCompilerDiagnostics = HasNoCompilerDiagnostics(string.Format(codeTemplate1, targetType, valueExpression), compilation);
if (hasCompilerDiagnostics)
{
return true;
}

if (!constantValue.HasValue || valueType == null)
{
return false;
}

var constantLiteral = FormatLiteral(constantValue.Value);
if (constantLiteral == null)
{
return false;
}

return HasNoCompilerDiagnostics(string.Format(codeTemplate2, targetType, valueType, constantLiteral), compilation);
}

private static bool HasNoCompilerDiagnostics(string code, Compilation compilation)
{
var syntaxTree = CSharpSyntaxTree.ParseText(code);

var compilerDiagnostics = compilation.AddSyntaxTrees(syntaxTree)
.GetSemanticModel(syntaxTree)
.GetMethodBodyDiagnostics()
.Where(d => d.DefaultSeverity == DiagnosticSeverity.Error)
.ToList();

return compilerDiagnostics.Count == 0;
}

private static string? FormatLiteral(object? value)
{
return value switch
{
byte b => b.ToString(),
sbyte sb => sb.ToString(),
short s => s.ToString(),
ushort us => us.ToString(),
int i => i.ToString(),
uint ui => $"{ui}U",
long l => $"{l}L",
ulong ul => $"{ul}UL",
float f => $"{f.ToString(CultureInfo.InvariantCulture)}F",
double d => $"{d.ToString(CultureInfo.InvariantCulture)}D",
decimal m => $"{m.ToString(CultureInfo.InvariantCulture)}M",
char c => $"'{c}'",
bool b => b ? "true" : "false",
string s => $"\"{s}\"",
null => "null",
_ => null
};
}

public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
{
key = tuple.Key;
value = tuple.Value;
}
}
}
2 changes: 2 additions & 0 deletions src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
Loading
Loading