Skip to content

Commit b1ebcb8

Browse files
authored
Multi target analyzers (#2866)
* Multi-target analyzers * Fix BDN1502 in net462. * Refactored ArgumentsAttributeAnalyzer to use semantic model before syntax model for cleaner testing of assignability. Added BDN1503. WIP need to do the same for Params. * Refactored ParamsAttributeAnalyzer to use semantic model. Updated to handle more cases. Reverted unknown types test removal. * Remove unused code. * Remove unnecessary pre-release MCC version.
1 parent fcb6d3f commit b1ebcb8

21 files changed

+395
-792
lines changed

build/BenchmarkDotNet.Build/BuildContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class BuildContext : FrostingContext
3030
public DirectoryPath ArtifactsDirectory { get; }
3131

3232
public FilePath SolutionFile { get; }
33+
public FilePath AnalyzersProjectFile { get; }
3334
public FilePath TemplatesTestsProjectFile { get; }
3435
public FilePathCollection AllPackableSrcProjects { get; }
3536
public FilePath VersionsFile { get; }
@@ -64,6 +65,7 @@ public BuildContext(ICakeContext context)
6465
context.Tools.RegisterFile(toolFilePath);
6566

6667
SolutionFile = RootDirectory.CombineWithFilePath("BenchmarkDotNet.sln");
68+
AnalyzersProjectFile = RootDirectory.Combine("src").Combine("BenchmarkDotNet.Analyzers").CombineWithFilePath("BenchmarkDotNet.Analyzers.csproj");
6769

6870
TemplatesTestsProjectFile = RootDirectory.Combine("templates")
6971
.CombineWithFilePath("BenchmarkDotNet.Templates.csproj");

build/BenchmarkDotNet.Build/Program.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,29 @@ public class AllTestsTask : FrostingTask<BuildContext>, IHelpProvider
156156
public HelpInfo GetHelp() => new();
157157
}
158158

159+
[TaskName(Name)]
160+
[TaskDescription("Build BenchmarkDotNet.Analyzers")]
161+
public class BuildAnalyzersTask : FrostingTask<BuildContext>, IHelpProvider
162+
{
163+
private const string Name = "build-analyzers";
164+
public override void Run(BuildContext context) => context.BuildRunner.BuildAnalyzers();
165+
166+
public HelpInfo GetHelp()
167+
{
168+
return new HelpInfo
169+
{
170+
Examples =
171+
[
172+
new Example(Name)
173+
]
174+
};
175+
}
176+
}
177+
159178
[TaskName(Name)]
160179
[TaskDescription("Pack Nupkg packages")]
161180
[IsDependentOn(typeof(BuildTask))]
181+
[IsDependentOn(typeof(BuildAnalyzersTask))]
162182
public class PackTask : FrostingTask<BuildContext>, IHelpProvider
163183
{
164184
private const string Name = "pack";

build/BenchmarkDotNet.Build/Runners/BuildRunner.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,24 @@ public void BuildProjectSilent(FilePath projectFile)
6464
});
6565
}
6666

67+
public void BuildAnalyzers()
68+
{
69+
context.Information("BuildSystemProvider: " + context.BuildSystem().Provider);
70+
string[] mccVersions = ["2.8", "3.8", "4.8"];
71+
foreach (string version in mccVersions)
72+
{
73+
context.DotNetBuild(context.AnalyzersProjectFile.FullPath, new DotNetBuildSettings
74+
{
75+
NoRestore = true,
76+
DiagnosticOutput = true,
77+
MSBuildSettings = context.MsBuildSettingsBuild,
78+
Configuration = context.BuildConfiguration,
79+
Verbosity = context.BuildVerbosity,
80+
ArgumentCustomization = args => args.Append($"-p:MccVersion={version}")
81+
});
82+
}
83+
}
84+
6785
public void Pack()
6886
{
6987
context.CleanDirectory(context.ArtifactsDirectory);

build/common.props

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
<Nullable>annotations</Nullable>
2626
<!-- Suppress warning for nuget package used in old (unsupported) tfm. -->
2727
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
28-
<NoWarn>CS9057</NoWarn>
2928
</PropertyGroup>
3029

3130
<ItemGroup>

src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs

Lines changed: 64 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.CodeAnalysis.CSharp.Syntax;
44
using System.Collections.Generic;
55
using System.Collections.Immutable;
6+
using System.Diagnostics;
67
using System.Globalization;
78
using System.Linq;
89

@@ -18,9 +19,6 @@ public static LocalizableResourceString GetResourceString(string name)
1819
public static INamedTypeSymbol? GetBenchmarkAttributeTypeSymbol(Compilation compilation)
1920
=> compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.BenchmarkAttribute");
2021

21-
public static bool AttributeListsContainAttribute(string attributeName, Compilation compilation, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
22-
=> AttributeListsContainAttribute(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);
23-
2422
public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTypeSymbol, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
2523
{
2624
if (attributeTypeSymbol == null || attributeTypeSymbol.TypeKind == TypeKind.Error)
@@ -38,7 +36,7 @@ public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTyp
3836
continue;
3937
}
4038

41-
if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
39+
if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol))
4240
{
4341
return true;
4442
}
@@ -58,7 +56,7 @@ public static bool AttributeListContainsAttribute(INamedTypeSymbol? attributeTyp
5856
return false;
5957
}
6058

61-
return attributeList.Any(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default));
59+
return attributeList.Any(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(attributeTypeSymbol));
6260
}
6361

6462
public static ImmutableArray<AttributeSyntax> GetAttributes(string attributeName, Compilation compilation, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
@@ -83,7 +81,7 @@ public static ImmutableArray<AttributeSyntax> GetAttributes(INamedTypeSymbol? at
8381
continue;
8482
}
8583

86-
if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
84+
if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol))
8785
{
8886
attributesBuilder.Add(attributeSyntax);
8987
}
@@ -93,38 +91,6 @@ public static ImmutableArray<AttributeSyntax> GetAttributes(INamedTypeSymbol? at
9391
return attributesBuilder.ToImmutable();
9492
}
9593

96-
public static int GetAttributeUsageCount(string attributeName, Compilation compilation, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
97-
=> GetAttributeUsageCount(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel);
98-
99-
public static int GetAttributeUsageCount(INamedTypeSymbol? attributeTypeSymbol, SyntaxList<AttributeListSyntax> attributeLists, SemanticModel semanticModel)
100-
{
101-
var attributeUsageCount = 0;
102-
103-
if (attributeTypeSymbol == null)
104-
{
105-
return 0;
106-
}
107-
108-
foreach (var attributeListSyntax in attributeLists)
109-
{
110-
foreach (var attributeSyntax in attributeListSyntax.Attributes)
111-
{
112-
var attributeSyntaxTypeSymbol = semanticModel.GetTypeInfo(attributeSyntax).Type;
113-
if (attributeSyntaxTypeSymbol == null)
114-
{
115-
continue;
116-
}
117-
118-
if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
119-
{
120-
attributeUsageCount++;
121-
}
122-
}
123-
}
124-
125-
return attributeUsageCount;
126-
}
127-
12894
public static string NormalizeTypeName(INamedTypeSymbol namedTypeSymbol)
12995
{
13096
string typeName;
@@ -145,119 +111,86 @@ public static string NormalizeTypeName(INamedTypeSymbol namedTypeSymbol)
145111
return typeName;
146112
}
147113

148-
public static bool IsAssignableToField(Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
114+
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
149115
{
150-
const string codeTemplate1 = """
151-
{0}
152-
153-
file static class Internal {{
154-
static readonly {1} x = {2};
155-
}}
156-
""";
157-
158-
const string codeTemplate2 = """
159-
{0}
160-
161-
file static class Internal {{
162-
static readonly {1} x = ({2}){3};
163-
}}
164-
""";
165-
166-
return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, languageVersion, valueTypeContainingNamespace, targetType, valueExpression, constantValue, valueType);
116+
key = tuple.Key;
117+
value = tuple.Value;
167118
}
168119

169-
public static bool IsAssignableToLocal(Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
170-
{
171-
const string codeTemplate1 = """
172-
{0}
173-
174-
file static class Internal {{
175-
static void Method() {{
176-
{1} x = {2};
177-
}}
178-
}}
179-
""";
180-
181-
const string codeTemplate2 = """
182-
{0}
183-
184-
file static class Internal {{
185-
static void Method() {{
186-
{1} x = ({2}){3};
187-
}}
188-
}}
189-
""";
120+
public static Location GetLocation(this AttributeData attributeData)
121+
=> attributeData.ApplicationSyntaxReference.SyntaxTree.GetLocation(attributeData.ApplicationSyntaxReference.Span);
190122

191-
return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, languageVersion, valueTypeContainingNamespace, targetType, valueExpression, constantValue, valueType);
192-
}
193-
194-
private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
123+
public static bool IsAssignable(TypedConstant constant, ExpressionSyntax expression, ITypeSymbol targetType, Compilation compilation)
195124
{
196-
var usingDirective = valueTypeContainingNamespace != null ? $"using {valueTypeContainingNamespace};" : "";
197-
198-
var hasNoCompilerDiagnostics = HasNoCompilerDiagnostics(string.Format(codeTemplate1, usingDirective, targetType, valueExpression), compilation, languageVersion);
199-
if (hasNoCompilerDiagnostics)
125+
if (constant.IsNull)
200126
{
201-
return true;
127+
// Check if targetType is a reference type or nullable.
128+
return targetType.IsReferenceType || targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
202129
}
203130

204-
if (!constantValue.HasValue || valueType == null)
131+
var sourceType = constant.Type;
132+
if (sourceType == null)
205133
{
206134
return false;
207135
}
208136

209-
var constantLiteral = FormatLiteral(constantValue.Value);
210-
if (constantLiteral == null)
137+
// Test if the constant type is implicitly assignable.
138+
var conversion = compilation.ClassifyConversion(sourceType, targetType);
139+
if (conversion.IsImplicit)
211140
{
212-
return false;
141+
return true;
213142
}
214143

215-
return HasNoCompilerDiagnostics(string.Format(codeTemplate2, usingDirective, targetType, valueType, constantLiteral), compilation, languageVersion);
144+
// Int32 values fail the test to smaller types, but it's still valid in the generated code to assign the literal to a smaller integer type,
145+
// so test if the expression is implicitly assignable.
146+
var semanticModel = compilation.GetSemanticModel(expression.SyntaxTree);
147+
// Only enums use explicit casting, so we test with explicit cast only for enums. See BenchmarkConverter.Map(...).
148+
bool isEnum = targetType.TypeKind == TypeKind.Enum;
149+
// The existing implementation only checks for direct enum type, not Nullable<TEnum>, so we won't check it here either unless BenchmarkConverter gets updated to handle it.
150+
//bool isNullableEnum =
151+
// targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
152+
// targetType is INamedTypeSymbol named &&
153+
// named.TypeArguments.Length == 1 &&
154+
// named.TypeArguments[0].TypeKind == TypeKind.Enum;
155+
conversion = semanticModel.ClassifyConversion(expression, targetType, isEnum);
156+
if (conversion.IsImplicit)
157+
{
158+
return true;
159+
}
160+
return isEnum && conversion.IsExplicit;
216161
}
217162

218-
private static bool HasNoCompilerDiagnostics(string code, Compilation compilation, LanguageVersion languageVersion)
163+
// Assumes a single `params object[] values` constructor
164+
public static ExpressionSyntax GetAttributeParamsArgumentExpression(this AttributeData attributeData, int index)
219165
{
220-
var compilationTestSyntaxTree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(languageVersion));
221-
222-
var syntaxTreesWithInterceptorsNamespaces = compilation.SyntaxTrees.Where(st => st.Options.Features.ContainsKey(InterceptorsNamespaces));
223-
224-
var compilerDiagnostics = compilation
225-
.RemoveSyntaxTrees(syntaxTreesWithInterceptorsNamespaces)
226-
.AddSyntaxTrees(compilationTestSyntaxTree)
227-
.GetSemanticModel(compilationTestSyntaxTree)
228-
.GetMethodBodyDiagnostics()
229-
.Where(d => d.DefaultSeverity == DiagnosticSeverity.Error)
230-
.ToList();
231-
232-
return compilerDiagnostics.Count == 0;
233-
}
166+
Debug.Assert(index >= 0);
167+
// Properties must come after constructor arguments, so we don't need to worry about it here.
168+
var attrSyntax = (AttributeSyntax) attributeData.ApplicationSyntaxReference.GetSyntax();
169+
var args = attrSyntax.ArgumentList.Arguments;
170+
Debug.Assert(args is { Count: > 0 });
171+
var maybeArrayExpression = args[0].Expression;
172+
173+
#if CODE_ANALYSIS_4_8
174+
if (maybeArrayExpression is CollectionExpressionSyntax collectionExpressionSyntax)
175+
{
176+
Debug.Assert(index < collectionExpressionSyntax.Elements.Count);
177+
return ((ExpressionElementSyntax) collectionExpressionSyntax.Elements[index]).Expression;
178+
}
179+
#endif
234180

235-
private static string? FormatLiteral(object? value)
236-
{
237-
return value switch
181+
if (maybeArrayExpression is ArrayCreationExpressionSyntax arrayCreationExpressionSyntax)
238182
{
239-
byte b => b.ToString(),
240-
sbyte sb => sb.ToString(),
241-
short s => s.ToString(),
242-
ushort us => us.ToString(),
243-
int i => i.ToString(),
244-
uint ui => $"{ui}U",
245-
long l => $"{l}L",
246-
ulong ul => $"{ul}UL",
247-
float f => $"{f.ToString(CultureInfo.InvariantCulture)}F",
248-
double d => $"{d.ToString(CultureInfo.InvariantCulture)}D",
249-
decimal m => $"{m.ToString(CultureInfo.InvariantCulture)}M",
250-
char c => $"'{c}'",
251-
bool b => b ? "true" : "false",
252-
string s => $"\"{s}\"",
253-
null => "null",
254-
_ => null
255-
};
256-
}
183+
if (arrayCreationExpressionSyntax.Initializer == null)
184+
{
185+
return maybeArrayExpression;
186+
}
187+
Debug.Assert(index < arrayCreationExpressionSyntax.Initializer.Expressions.Count);
188+
return arrayCreationExpressionSyntax.Initializer.Expressions[index];
189+
}
257190

258-
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
259-
{
260-
key = tuple.Key;
261-
value = tuple.Value;
191+
// Params values
192+
Debug.Assert(index < args.Count);
193+
Debug.Assert(args[index].NameEquals is null);
194+
return args[index].Expression;
262195
}
263196
}

src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
### New Rules
55

6+
Rule ID | Category | Severity | Notes
7+
---------|----------|----------|--------------------
8+
BDN1503 | Usage | Error | BDN1503_Attributes_ArgumentsAttribute_RequiresParameters
9+
10+
### New Rules
11+
612
Rule ID | Category | Severity | Notes
713
---------|----------|----------|--------------------
814
BDN1000 | Usage | Error | BDN1000_BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods

0 commit comments

Comments
 (0)