Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions build/BenchmarkDotNet.Build/BuildContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class BuildContext : FrostingContext
public DirectoryPath ArtifactsDirectory { get; }

public FilePath SolutionFile { get; }
public FilePath AnalyzersProjectFile { get; }
public FilePath TemplatesTestsProjectFile { get; }
public FilePathCollection AllPackableSrcProjects { get; }
public FilePath VersionsFile { get; }
Expand Down Expand Up @@ -64,6 +65,7 @@ public BuildContext(ICakeContext context)
context.Tools.RegisterFile(toolFilePath);

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

TemplatesTestsProjectFile = RootDirectory.Combine("templates")
.CombineWithFilePath("BenchmarkDotNet.Templates.csproj");
Expand Down
20 changes: 20 additions & 0 deletions build/BenchmarkDotNet.Build/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,29 @@ public class AllTestsTask : FrostingTask<BuildContext>, IHelpProvider
public HelpInfo GetHelp() => new();
}

[TaskName(Name)]
[TaskDescription("Build BenchmarkDotNet.Analyzers")]
public class BuildAnalyzersTask : FrostingTask<BuildContext>, IHelpProvider
{
private const string Name = "build-analyzers";
public override void Run(BuildContext context) => context.BuildRunner.BuildAnalyzers();

public HelpInfo GetHelp()
{
return new HelpInfo
{
Examples =
[
new Example(Name)
]
};
}
}

[TaskName(Name)]
[TaskDescription("Pack Nupkg packages")]
[IsDependentOn(typeof(BuildTask))]
[IsDependentOn(typeof(BuildAnalyzersTask))]
public class PackTask : FrostingTask<BuildContext>, IHelpProvider
{
private const string Name = "pack";
Expand Down
18 changes: 18 additions & 0 deletions build/BenchmarkDotNet.Build/Runners/BuildRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ public void BuildProjectSilent(FilePath projectFile)
});
}

public void BuildAnalyzers()
{
context.Information("BuildSystemProvider: " + context.BuildSystem().Provider);
string[] mccVersions = ["2.8", "3.8", "4.8"];
foreach (string version in mccVersions)
{
context.DotNetBuild(context.AnalyzersProjectFile.FullPath, new DotNetBuildSettings
{
NoRestore = true,
DiagnosticOutput = true,
MSBuildSettings = context.MsBuildSettingsBuild,
Configuration = context.BuildConfiguration,
Verbosity = context.BuildVerbosity,
ArgumentCustomization = args => args.Append($"-p:MccVersion={version}")
});
}
}

public void Pack()
{
context.CleanDirectory(context.ArtifactsDirectory);
Expand Down
1 change: 0 additions & 1 deletion build/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
<Nullable>annotations</Nullable>
<!-- Suppress warning for nuget package used in old (unsupported) tfm. -->
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<NoWarn>CS9057</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
195 changes: 64 additions & 131 deletions src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Linq;

Expand All @@ -18,9 +19,6 @@ public static LocalizableResourceString GetResourceString(string name)
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)
Expand All @@ -38,7 +36,7 @@ public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTyp
continue;
}

if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol))
{
return true;
}
Expand All @@ -58,7 +56,7 @@ public static bool AttributeListContainsAttribute(INamedTypeSymbol? attributeTyp
return false;
}

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

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

if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default))
if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol))
{
attributesBuilder.Add(attributeSyntax);
}
Expand All @@ -93,38 +91,6 @@ public static ImmutableArray<AttributeSyntax> GetAttributes(INamedTypeSymbol? at
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;
Expand All @@ -145,119 +111,86 @@ public static string NormalizeTypeName(INamedTypeSymbol namedTypeSymbol)
return typeName;
}

public static bool IsAssignableToField(Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
{
const string codeTemplate1 = """
{0}

file static class Internal {{
static readonly {1} x = {2};
}}
""";

const string codeTemplate2 = """
{0}

file static class Internal {{
static readonly {1} x = ({2}){3};
}}
""";

return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, languageVersion, valueTypeContainingNamespace, targetType, valueExpression, constantValue, valueType);
key = tuple.Key;
value = tuple.Value;
}

public static bool IsAssignableToLocal(Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
{
const string codeTemplate1 = """
{0}

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

const string codeTemplate2 = """
{0}

file static class Internal {{
static void Method() {{
{1} x = ({2}){3};
}}
}}
""";
public static Location GetLocation(this AttributeData attributeData)
=> attributeData.ApplicationSyntaxReference.SyntaxTree.GetLocation(attributeData.ApplicationSyntaxReference.Span);

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

private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional<object?> constantValue, string? valueType)
public static bool IsAssignable(TypedConstant constant, ExpressionSyntax expression, ITypeSymbol targetType, Compilation compilation)
{
var usingDirective = valueTypeContainingNamespace != null ? $"using {valueTypeContainingNamespace};" : "";

var hasNoCompilerDiagnostics = HasNoCompilerDiagnostics(string.Format(codeTemplate1, usingDirective, targetType, valueExpression), compilation, languageVersion);
if (hasNoCompilerDiagnostics)
if (constant.IsNull)
{
return true;
// Check if targetType is a reference type or nullable.
return targetType.IsReferenceType || targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
}

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

var constantLiteral = FormatLiteral(constantValue.Value);
if (constantLiteral == null)
// Test if the constant type is implicitly assignable.
var conversion = compilation.ClassifyConversion(sourceType, targetType);
if (conversion.IsImplicit)
{
return false;
return true;
}

return HasNoCompilerDiagnostics(string.Format(codeTemplate2, usingDirective, targetType, valueType, constantLiteral), compilation, languageVersion);
// 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,
// so test if the expression is implicitly assignable.
var semanticModel = compilation.GetSemanticModel(expression.SyntaxTree);
// Only enums use explicit casting, so we test with explicit cast only for enums. See BenchmarkConverter.Map(...).
bool isEnum = targetType.TypeKind == TypeKind.Enum;
// 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.
//bool isNullableEnum =
// targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
// targetType is INamedTypeSymbol named &&
// named.TypeArguments.Length == 1 &&
// named.TypeArguments[0].TypeKind == TypeKind.Enum;
conversion = semanticModel.ClassifyConversion(expression, targetType, isEnum);
if (conversion.IsImplicit)
{
return true;
}
return isEnum && conversion.IsExplicit;
}

private static bool HasNoCompilerDiagnostics(string code, Compilation compilation, LanguageVersion languageVersion)
// Assumes a single `params object[] values` constructor
public static ExpressionSyntax GetAttributeParamsArgumentExpression(this AttributeData attributeData, int index)
{
var compilationTestSyntaxTree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(languageVersion));

var syntaxTreesWithInterceptorsNamespaces = compilation.SyntaxTrees.Where(st => st.Options.Features.ContainsKey(InterceptorsNamespaces));

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

return compilerDiagnostics.Count == 0;
}
Debug.Assert(index >= 0);
// Properties must come after constructor arguments, so we don't need to worry about it here.
var attrSyntax = (AttributeSyntax) attributeData.ApplicationSyntaxReference.GetSyntax();
var args = attrSyntax.ArgumentList.Arguments;
Debug.Assert(args is { Count: > 0 });
var maybeArrayExpression = args[0].Expression;

#if CODE_ANALYSIS_4_8
if (maybeArrayExpression is CollectionExpressionSyntax collectionExpressionSyntax)
{
Debug.Assert(index < collectionExpressionSyntax.Elements.Count);
return ((ExpressionElementSyntax) collectionExpressionSyntax.Elements[index]).Expression;
}
#endif

private static string? FormatLiteral(object? value)
{
return value switch
if (maybeArrayExpression is ArrayCreationExpressionSyntax arrayCreationExpressionSyntax)
{
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
};
}
if (arrayCreationExpressionSyntax.Initializer == null)
{
return maybeArrayExpression;
}
Debug.Assert(index < arrayCreationExpressionSyntax.Initializer.Expressions.Count);
return arrayCreationExpressionSyntax.Initializer.Expressions[index];
}

public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
{
key = tuple.Key;
value = tuple.Value;
// Params values
Debug.Assert(index < args.Count);
Debug.Assert(args[index].NameEquals is null);
return args[index].Expression;
}
}
6 changes: 6 additions & 0 deletions src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

### New Rules

Rule ID | Category | Severity | Notes
---------|----------|----------|--------------------
BDN1503 | Usage | Error | BDN1503_Attributes_ArgumentsAttribute_RequiresParameters

### New Rules

Rule ID | Category | Severity | Notes
---------|----------|----------|--------------------
BDN1000 | Usage | Error | BDN1000_BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods
Expand Down
Loading