Skip to content

Commit f6b41a9

Browse files
committed
* Add support to analyze a boolean constant value for the Baseline property on BenchmarkAttribute
* Split logic for baseline method analyzer into two rules, also verifying whether they're unique per category
1 parent 41942fa commit f6b41a9

File tree

9 files changed

+1859
-185
lines changed

9 files changed

+1859
-185
lines changed

src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.CodeAnalysis.CSharp;
55
using Microsoft.CodeAnalysis.CSharp.Syntax;
66

7+
using System.Collections.Generic;
78
using System.Collections.Immutable;
89
using System.Globalization;
910
using System.Linq;
@@ -86,6 +87,37 @@ public static ImmutableArray<AttributeSyntax> GetAttributes(INamedTypeSymbol? at
8687
return attributesBuilder.ToImmutable();
8788
}
8889

90+
public static int GetAttributeUsageCount(string attributeName, Compilation compilation, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel) => GetAttributeUsageCount(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);
91+
92+
public static int GetAttributeUsageCount(INamedTypeSymbol? attributeTypeSymbol, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
93+
{
94+
var attributeUsageCount = 0;
95+
96+
if (attributeTypeSymbol == null)
97+
{
98+
return 0;
99+
}
100+
101+
foreach (var attributeListSyntax in attributeLists)
102+
{
103+
foreach (var attributeSyntax in attributeListSyntax.Attributes)
104+
{
105+
var attributeSyntaxTypeSymbol = semanticModel.GetTypeInfo(attributeSyntax).Type;
106+
if (attributeSyntaxTypeSymbol == null)
107+
{
108+
continue;
109+
}
110+
111+
if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
112+
{
113+
attributeUsageCount++;
114+
}
115+
}
116+
}
117+
118+
return attributeUsageCount;
119+
}
120+
89121
public static string NormalizeTypeName(INamedTypeSymbol namedTypeSymbol)
90122
{
91123
string typeName;
@@ -146,8 +178,8 @@ static void Method() {{
146178

147179
private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
148180
{
149-
var hasNoCompilationErrors = HasNoCompilationErrors(string.Format(codeTemplate1, targetType, valueExpression), compilation);
150-
if (hasNoCompilationErrors)
181+
var hasCompilerDiagnostics = HasNoCompilerDiagnostics(string.Format(codeTemplate1, targetType, valueExpression), compilation);
182+
if (hasCompilerDiagnostics)
151183
{
152184
return true;
153185
}
@@ -163,20 +195,20 @@ private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, C
163195
return false;
164196
}
165197

166-
return HasNoCompilationErrors(string.Format(codeTemplate2, targetType, valueType, constantLiteral), compilation);
198+
return HasNoCompilerDiagnostics(string.Format(codeTemplate2, targetType, valueType, constantLiteral), compilation);
167199
}
168200

169-
private static bool HasNoCompilationErrors(string code, Compilation compilation)
201+
private static bool HasNoCompilerDiagnostics(string code, Compilation compilation)
170202
{
171203
var syntaxTree = CSharpSyntaxTree.ParseText(code);
172204

173-
var compilationErrors = compilation.AddSyntaxTrees(syntaxTree)
174-
.GetSemanticModel(syntaxTree)
175-
.GetMethodBodyDiagnostics()
176-
.Where(d => d.DefaultSeverity == DiagnosticSeverity.Error)
177-
.ToList();
205+
var compilerDiagnostics = compilation.AddSyntaxTrees(syntaxTree)
206+
.GetSemanticModel(syntaxTree)
207+
.GetMethodBodyDiagnostics()
208+
.Where(d => d.DefaultSeverity == DiagnosticSeverity.Error)
209+
.ToList();
178210

179-
return compilationErrors.Count == 0;
211+
return compilerDiagnostics.Count == 0;
180212
}
181213

182214
private static string? FormatLiteral(object? value)
@@ -201,5 +233,11 @@ private static bool HasNoCompilationErrors(string code, Compilation compilation)
201233
_ => null
202234
};
203235
}
236+
237+
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
238+
{
239+
key = tuple.Key;
240+
value = tuple.Value;
241+
}
204242
}
205243
}

src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ BDN1102 | Usage | Error | BDN1102_General_BenchmarkClass_GenericTypeArgum
1616
BDN1103 | Usage | Error | BDN1103_General_BenchmarkClass_MethodMustBePublic
1717
BDN1104 | Usage | Error | BDN1104_General_BenchmarkClass_MethodMustBeNonGeneric
1818
BDN1105 | Usage | Error | BDN1105_General_BenchmarkClass_ClassMustBeNonStatic
19-
BDN1106 | Usage | Error | BDN1106_General_BenchmarkClass_OnlyOneMethodCanBeBaseline
19+
BDN1106 | Usage | Error | BDN1106_General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed
20+
BDN1107 | Usage | Error | BDN1107_General_BenchmarkClass_OnlyOneMethodCanBeBaseline
21+
BDN1108 | Usage | Warning | BDN1108_General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory
2022
BDN1200 | Usage | Error | BDN1200_Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField
2123
BDN1201 | Usage | Error | BDN1201_Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty
2224
BDN1202 | Usage | Error | BDN1202_Attributes_GeneralParameterAttributes_FieldMustBePublic

src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs

Lines changed: 40 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,13 @@
196196
<value>Benchmark class '{0}' must be generic</value>
197197
</data>
198198
<data name="General_BenchmarkClass_OnlyOneMethodCanBeBaseline_MessageFormat" xml:space="preserve">
199-
<value>Only one benchmark method can be marked as baseline</value>
199+
<value>Only one benchmark method can be marked as baseline per class</value>
200+
</data>
201+
<data name="General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_MessageFormat" xml:space="preserve">
202+
<value>Only one benchmark method can be marked as baseline per class and category</value>
203+
</data>
204+
<data name="General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_MessageFormat" xml:space="preserve">
205+
<value>Passing a single null argument creates a null params array. Use multiple arguments (e.g., null, "SomeCategory", ...) or a non-null value instead.</value>
200206
</data>
201207
<data name="General_BenchmarkClass_MethodMustBePublic_Title" xml:space="preserve">
202208
<value>Benchmark methods must be public</value>
@@ -211,7 +217,13 @@
211217
<value>Benchmark classes annotated with a [GenericTypeArguments] attribute must be generic</value>
212218
</data>
213219
<data name="General_BenchmarkClass_OnlyOneMethodCanBeBaseline_Title" xml:space="preserve">
214-
<value>Only one benchmark method can be baseline</value>
220+
<value>Only one benchmark method can be baseline per class</value>
221+
</data>
222+
<data name="General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_Title" xml:space="preserve">
223+
<value>Only one benchmark method can be baseline per class and category</value>
224+
</data>
225+
<data name="General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_Title" xml:space="preserve">
226+
<value>Single null argument to the [BenchmarkCategory] attribute results in unintended null array</value>
215227
</data>
216228
<data name="Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Description" xml:space="preserve">
217229
<value>Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a field at any one time</value>

src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Microsoft.CodeAnalysis.Diagnostics;
77

88
using System.Collections.Immutable;
9-
using System.Linq;
109

1110
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1211
public class RunAnalyzer : DiagnosticAnalyzer

src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ public static class DiagnosticIds
1313
public const string General_BenchmarkClass_MethodMustBePublic = "BDN1103";
1414
public const string General_BenchmarkClass_MethodMustBeNonGeneric = "BDN1104";
1515
public const string General_BenchmarkClass_ClassMustBeNonStatic = "BDN1105";
16-
public const string General_BenchmarkClass_OnlyOneMethodCanBeBaseline = "BDN1106";
16+
public const string General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed = "BDN1106";
17+
public const string General_BenchmarkClass_OnlyOneMethodCanBeBaseline = "BDN1107";
18+
public const string General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory = "BDN1108";
1719
public const string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField = "BDN1200";
1820
public const string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty = "BDN1201";
1921
public const string Attributes_GeneralParameterAttributes_FieldMustBePublic = "BDN1202";

0 commit comments

Comments
 (0)