Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e0bd1d6
Add Roslyn analyzers to detect incorrect usage of BenchmarkDotNet
silkfire Sep 26, 2025
c18a417
Unify C# language version
silkfire Oct 10, 2025
19e6ff9
Remove Analyzers package projects
silkfire Oct 10, 2025
58ce544
Revert BenchmarkDotNet.Disassembler changes
silkfire Oct 10, 2025
86d3a9c
Reference Analyzers project from Annotations
silkfire Oct 10, 2025
770e190
Move Benchmark.Analyzers and Benchmark.Analyzers.Tests to correct dir…
silkfire Oct 10, 2025
9d2423c
Remove accidentally added package Microsoft.CodeAnalysis.NetAnalyzers…
silkfire Oct 11, 2025
6719209
* Benchmark classes annotated with a [GenericTypeArguments] attribute…
silkfire Oct 12, 2025
4292904
* Change diagnostic ID increment ordering
silkfire Oct 13, 2025
a8dab86
When determining whether a class has any benchmark methods, iterate t…
silkfire Oct 13, 2025
4ab0b37
Move "Benchmark class cannot be sealed" to Run analyzer
silkfire Oct 13, 2025
57df116
Move "Benchmark class must be public" to Run analyzer
silkfire Oct 13, 2025
c576530
Support analyzing overload of BenchmarkRunner.Run that takes a Type p…
silkfire Oct 15, 2025
4c57a4e
Remove requirement that a class must have at least one method annotat…
silkfire Oct 15, 2025
0ccab47
* Integer attribute values that fit within target type range should n…
silkfire Oct 16, 2025
485cc9b
Use a dummy syntax tree to test whether types are implicitly convertible
silkfire Oct 17, 2025
256f2aa
Move "Generic class must be abstract or annotated with a [GenericType…
silkfire Oct 17, 2025
58aee81
Add support to analyze implicit conversion from an array to a Span of…
silkfire Oct 17, 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
148 changes: 148 additions & 0 deletions src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
namespace BenchmarkDotNet.Analyzers
{
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using System.Collections.Immutable;
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 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)
{
var code = $$"""
file static class Internal {
static readonly {{targetType}} x = {{valueExpression}};
}
""";

var syntaxTree = CSharpSyntaxTree.ParseText(code);

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

return compilationErrors.Count == 0;
}

public static bool IsAssignableToLocal(Compilation compilation, ITypeSymbol targetType, string valueExpression)
{
var code = $$"""
file static class Internal {
static Internal() {
{{targetType}} x = {{valueExpression}};
}
}
""";

var syntaxTree = CSharpSyntaxTree.ParseText(code);

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

return compilationErrors.Count == 0;
}
}
}
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
36 changes: 36 additions & 0 deletions src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
---------|----------|----------|--------------------
BDN1000 | Usage | Error | BDN1000_BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods
BDN1001 | Usage | Error | BDN1001_BenchmarkRunner_Run_TypeArgumentClassMustBePublic
BDN1002 | Usage | Error | BDN1002_BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed
BDN1003 | Usage | Error | BDN1003_BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract
BDN1004 | Usage | Error | BDN1004_BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute
BDN1100 | Usage | Error | BDN1100_General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract
BDN1101 | Usage | Error | BDN1101_General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric
BDN1102 | Usage | Error | BDN1102_General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount
BDN1103 | Usage | Error | BDN1103_General_BenchmarkClass_MethodMustBePublic
BDN1104 | Usage | Error | BDN1104_General_BenchmarkClass_MethodMustBeNonGeneric
BDN1105 | Usage | Error | BDN1105_General_BenchmarkClass_ClassMustBeNonStatic
BDN1106 | Usage | Error | BDN1106_General_BenchmarkClass_OnlyOneMethodCanBeBaseline
BDN1200 | Usage | Error | BDN1200_Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField
BDN1201 | Usage | Error | BDN1201_Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty
BDN1202 | Usage | Error | BDN1202_Attributes_GeneralParameterAttributes_FieldMustBePublic
BDN1203 | Usage | Error | BDN1203_Attributes_GeneralParameterAttributes_PropertyMustBePublic
BDN1204 | Usage | Error | BDN1204_Attributes_GeneralParameterAttributes_NotValidOnReadonlyField
BDN1205 | Usage | Error | BDN1205_Attributes_GeneralParameterAttributes_NotValidOnConstantField
BDN1206 | Usage | Error | BDN1206_Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly
BDN1207 | Usage | Error | BDN1207_Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter
BDN1300 | Usage | Error | BDN1300_Attributes_ParamsAttribute_MustHaveValues
BDN1301 | Usage | Error | BDN1301_Attributes_ParamsAttribute_UnexpectedValueType
BDN1302 | Usage | Warning | BDN1302_Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute
BDN1303 | Usage | Error | BDN1303_Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType
BDN1304 | Usage | Error | BDN1304_Attributes_ParamsAllValues_PropertyOrFieldTypeMustBeEnumOrBool
BDN1400 | Usage | Error | BDN1400_Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute
BDN1401 | Usage | Error | BDN1401_Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters
BDN1402 | Usage | Error | BDN1402_Attributes_ArgumentsAttribute_MustHaveMatchingValueCount
BDN1403 | Usage | Error | BDN1403_Attributes_ArgumentsAttribute_MustHaveMatchingValueType
Loading