diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml
index f893d46147..a0b3ee4d30 100644
--- a/.github/workflows/run-tests.yaml
+++ b/.github/workflows/run-tests.yaml
@@ -45,6 +45,12 @@ jobs:
- name: Run task 'build'
shell: cmd
run: ./build.cmd build
+ - name: Run task 'unit-tests'
+ shell: cmd
+ run: ./build.cmd unit-tests -e
+ - name: Run task 'analyzer-tests'
+ shell: cmd
+ run: ./build.cmd analyzer-tests -e
- name: Run task 'in-tests-full'
shell: cmd
run: ./build.cmd in-tests-full -e
@@ -79,6 +85,8 @@ jobs:
# Build and Test
- name: Run task 'build'
run: ./build.cmd build
+ - name: Run task 'analyzer-tests'
+ run: ./build.cmd analyzer-tests -e
- name: Run task 'unit-tests'
run: ./build.cmd unit-tests -e
- name: Run task 'in-tests-core'
@@ -116,6 +124,8 @@ jobs:
# Build and Test
- name: Run task 'build'
run: ./build.cmd build
+ - name: Run task 'analyzer-tests'
+ run: ./build.cmd analyzer-tests -e
- name: Run task 'unit-tests'
run: ./build.cmd unit-tests -e
- name: Run task 'in-tests-core'
diff --git a/.gitignore b/.gitignore
index 96012efb78..ac368b610f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,6 +54,9 @@ src/BenchmarkDotNet/Disassemblers/BenchmarkDotNet.Disassembler.*.nupkg
# Visual Studio 2015 cache/options directory
.vs/
+# VSCode directory
+.vscode/
+
# Cake
tools/**
.dotnet
diff --git a/BenchmarkDotNet.sln b/BenchmarkDotNet.sln
index 1df6c0aabd..2ca0af1119 100644
--- a/BenchmarkDotNet.sln
+++ b/BenchmarkDotNet.sln
@@ -59,6 +59,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.P
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Exporters.Plotting.Tests", "tests\BenchmarkDotNet.Exporters.Plotting.Tests\BenchmarkDotNet.Exporters.Plotting.Tests.csproj", "{199AC83E-30BD-40CD-87CE-0C838AC0320D}"
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
@@ -161,6 +165,14 @@ Global
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{199AC83E-30BD-40CD-87CE-0C838AC0320D}.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
@@ -190,6 +202,8 @@ Global
{2E2283A3-6DA6-4482-8518-99D6D9F689AB} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
{B92ECCEF-7C27-4012-9E19-679F3C40A6A6} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
{199AC83E-30BD-40CD-87CE-0C838AC0320D} = {14195214-591A-45B7-851A-19D3BA2413F9}
+ {AA4DDCA0-C1D8-ADA8-69FE-2F67C4CA96B1} = {D6597E3A-6892-4A68-8E14-042FC941FDA2}
+ {7DE89F16-2160-42E3-004E-1F5064732121} = {14195214-591A-45B7-851A-19D3BA2413F9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4D9AF12B-1F7F-45A7-9E8C-E4E46ADCBD1F}
diff --git a/NuGet.Config b/NuGet.Config
index 7507704b8b..0ed38438e6 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -1,18 +1,21 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/BenchmarkDotNet.Build/Program.cs b/build/BenchmarkDotNet.Build/Program.cs
index 031e74266f..825d1e1554 100644
--- a/build/BenchmarkDotNet.Build/Program.cs
+++ b/build/BenchmarkDotNet.Build/Program.cs
@@ -98,6 +98,28 @@ public HelpInfo GetHelp()
}
}
+[TaskName(Name)]
+[TaskDescription("Run analyzer tests")]
+[IsDependentOn(typeof(BuildTask))]
+public class AnalyzerTestsTask : FrostingTask, IHelpProvider
+{
+ private const string Name = "analyzer-tests";
+ public override void Run(BuildContext context) => context.UnitTestRunner.RunAnalyzerTests();
+
+ public HelpInfo GetHelp()
+ {
+ return new HelpInfo
+ {
+ Examples =
+ [
+ new Example(Name)
+ .WithArgument(KnownOptions.Exclusive)
+ .WithArgument(KnownOptions.Verbosity, "Diagnostic")
+ ]
+ };
+ }
+}
+
[TaskName(Name)]
[TaskDescription("Run integration tests using .NET Framework 4.6.2+ (slow)")]
[IsDependentOn(typeof(BuildTask))]
@@ -123,8 +145,9 @@ public class InTestsCoreTask : FrostingTask, IHelpProvider
}
[TaskName(Name)]
-[TaskDescription("Run all unit and integration tests (slow)")]
+[TaskDescription("Run all unit, analyzer, and integration tests (slow)")]
[IsDependentOn(typeof(UnitTestsTask))]
+[IsDependentOn(typeof(AnalyzerTestsTask))]
[IsDependentOn(typeof(InTestsFullTask))]
[IsDependentOn(typeof(InTestsCoreTask))]
public class AllTestsTask : FrostingTask, IHelpProvider
diff --git a/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs b/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs
index 89b777d09f..a62960d3ca 100644
--- a/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs
+++ b/build/BenchmarkDotNet.Build/Runners/UnitTestRunner.cs
@@ -20,6 +20,11 @@ public class UnitTestRunner(BuildContext context)
.Combine("BenchmarkDotNet.Exporters.Plotting.Tests")
.CombineWithFilePath("BenchmarkDotNet.Exporters.Plotting.Tests.csproj");
+ private FilePath AnalyzerTestsProjectFile { get; } = context.RootDirectory
+ .Combine("tests")
+ .Combine("BenchmarkDotNet.Analyzers.Tests")
+ .CombineWithFilePath("BenchmarkDotNet.Analyzers.Tests.csproj");
+
private FilePath IntegrationTestsProjectFile { get; } = context.RootDirectory
.Combine("tests")
.Combine("BenchmarkDotNet.IntegrationTests")
@@ -70,5 +75,12 @@ public void RunUnitTests()
RunUnitTests(targetFramework);
}
+ public void RunAnalyzerTests()
+ {
+ string[] targetFrameworks = context.IsRunningOnWindows() ? ["net462", "net8.0"] : ["net8.0"];
+ foreach (var targetFramework in targetFrameworks)
+ RunTests(AnalyzerTestsProjectFile, "analyzer", targetFramework);
+ }
+
public void RunInTests(string tfm) => RunTests(IntegrationTestsProjectFile, "integration", tfm);
}
\ No newline at end of file
diff --git a/build/common.props b/build/common.props
index b5c0b81959..9d62d00ae4 100644
--- a/build/common.props
+++ b/build/common.props
@@ -21,10 +21,11 @@
false
$(MSBuildThisFileDirectory)CodingStyle.ruleset
true
-
+ NU1900
annotations
true
+ CS9057
diff --git a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj
index 948849aa05..f7b83d0a9d 100644
--- a/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj
+++ b/samples/BenchmarkDotNet.Samples/BenchmarkDotNet.Samples.csproj
@@ -40,4 +40,8 @@
+
+
+
+
diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs
new file mode 100644
index 0000000000..d23ca9db89
--- /dev/null
+++ b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs
@@ -0,0 +1,248 @@
+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;
+
+namespace BenchmarkDotNet.Analyzers;
+
+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 attributeLists, SemanticModel semanticModel)
+ => AttributeListsContainAttribute(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);
+
+ public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTypeSymbol, SyntaxList 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 attributeList)
+ => AttributeListContainsAttribute(compilation.GetTypeByMetadataName(attributeName), attributeList);
+
+ public static bool AttributeListContainsAttribute(INamedTypeSymbol? attributeTypeSymbol, ImmutableArray 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 GetAttributes(string attributeName, Compilation compilation, SyntaxList attributeLists, SemanticModel semanticModel)
+ => GetAttributes(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);
+
+ public static ImmutableArray GetAttributes(INamedTypeSymbol? attributeTypeSymbol, SyntaxList attributeLists, SemanticModel semanticModel)
+ {
+ var attributesBuilder = ImmutableArray.CreateBuilder();
+
+ 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 attributeLists, SemanticModel semanticModel)
+ => GetAttributeUsageCount(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);
+
+ public static int GetAttributeUsageCount(INamedTypeSymbol? attributeTypeSymbol, SyntaxList 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