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.Analyzers.sln b/BenchmarkDotNet.Analyzers.sln new file mode 100644 index 0000000000..2398ad7d66 --- /dev/null +++ b/BenchmarkDotNet.Analyzers.sln @@ -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 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/common.props b/build/common.props index bb8b1ad253..c22f8f841c 100644 --- a/build/common.props +++ b/build/common.props @@ -21,7 +21,7 @@ false $(MSBuildThisFileDirectory)CodingStyle.ruleset true - + NU1900 annotations true diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs new file mode 100644 index 0000000000..597181715a --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs @@ -0,0 +1,205 @@ +namespace BenchmarkDotNet.Analyzers +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + using System.Collections.Immutable; + using System.Globalization; + 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 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 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 constantValue, string? valueType) + { + const string codeTemplate1 = """ + file static class Internal {{ + static readonly {0} x = {1}; + }} + """; + + const string codeTemplate2 = """ + file static class Internal {{ + static readonly {0} x = ({1}){2}; + }} + """; + + return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, targetType, valueExpression, constantValue, valueType); + } + + public static bool IsAssignableToLocal(Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) + { + const string codeTemplate1 = """ + file static class Internal {{ + static void Method() {{ + {0} x = {1}; + }} + }} + """; + + const string codeTemplate2 = """ + file static class Internal {{ + static void Method() {{ + {0} x = ({1}){2}; + }} + }} + """; + + return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, targetType, valueExpression, constantValue, valueType); + } + + private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) + { + var hasNoCompilationErrors = HasNoCompilationErrors(string.Format(codeTemplate1, targetType, valueExpression), compilation); + if (hasNoCompilationErrors) + { + return true; + } + + if (!constantValue.HasValue || valueType == null) + { + return false; + } + + var constantLiteral = FormatLiteral(constantValue.Value); + if (constantLiteral == null) + { + return false; + } + + return HasNoCompilationErrors(string.Format(codeTemplate2, targetType, valueType, constantLiteral), compilation); + } + + private static bool HasNoCompilationErrors(string code, Compilation compilation) + { + var syntaxTree = CSharpSyntaxTree.ParseText(code); + + var compilationErrors = compilation.AddSyntaxTrees(syntaxTree) + .GetSemanticModel(syntaxTree) + .GetMethodBodyDiagnostics() + .Where(d => d.DefaultSeverity == DiagnosticSeverity.Error) + .ToList(); + + return compilationErrors.Count == 0; + } + + private static string? FormatLiteral(object? value) + { + return value switch + { + 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 + }; + } + } +} diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Shipped.md b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..ab7ee321e4 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..947dbb391c --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md @@ -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_MustHaveMatchingValueType +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 diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs new file mode 100644 index 0000000000..9a6d9dfe05 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs @@ -0,0 +1,345 @@ +namespace BenchmarkDotNet.Analyzers.Attributes +{ + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.Diagnostics; + using Microsoft.CodeAnalysis.Text; + + using System; + using System.Collections.Immutable; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ArgumentsAttributeAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor RequiresBenchmarkAttributeRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor MethodWithoutAttributeMustHaveNoParametersRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_Description); + + internal static readonly DiagnosticDescriptor MustHaveMatchingValueCountRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ArgumentsAttribute_MustHaveMatchingValueCount, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Description))); + + internal static readonly DiagnosticDescriptor MustHaveMatchingValueTypeRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ArgumentsAttribute_MustHaveMatchingValueType, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueType_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description))); + + public override ImmutableArray SupportedDiagnostics => + [ + RequiresBenchmarkAttributeRule, + MethodWithoutAttributeMustHaveNoParametersRule, + MustHaveMatchingValueCountRule, + MustHaveMatchingValueTypeRule + ]; + + public override void Initialize(AnalysisContext analysisContext) + { + analysisContext.EnableConcurrentExecution(); + analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + analysisContext.RegisterCompilationStartAction(ctx => + { + // Only run if BenchmarkDotNet.Annotations is referenced + var benchmarkAttributeTypeSymbol = AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(ctx.Compilation); + if (benchmarkAttributeTypeSymbol == null) + { + return; + } + + ctx.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration); + }); + } + + private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) + { + if (context.Node is not MethodDeclarationSyntax methodDeclarationSyntax) + { + return; + } + + var argumentsAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ArgumentsAttribute"); + if (argumentsAttributeTypeSymbol == null) + { + return; + } + + var hasBenchmarkAttribute = AnalyzerHelper.AttributeListsContainAttribute(AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(context.Compilation), methodDeclarationSyntax.AttributeLists, context.SemanticModel); + + var argumentsAttributes = AnalyzerHelper.GetAttributes(argumentsAttributeTypeSymbol, methodDeclarationSyntax.AttributeLists, context.SemanticModel); + if (argumentsAttributes.Length == 0) + { + if (hasBenchmarkAttribute && methodDeclarationSyntax.ParameterList.Parameters.Count > 0) + { + context.ReportDiagnostic(Diagnostic.Create(MethodWithoutAttributeMustHaveNoParametersRule, Location.Create(context.FilterTree, methodDeclarationSyntax.ParameterList.Parameters.Span), methodDeclarationSyntax.Identifier.ToString())); + } + + return; + } + + if (!hasBenchmarkAttribute) + { + foreach (var argumentsAttributeSyntax in argumentsAttributes) + { + context.ReportDiagnostic(Diagnostic.Create(RequiresBenchmarkAttributeRule, argumentsAttributeSyntax.GetLocation())); + } + + return; + } + + var methodParameterTypeSymbolsBuilder = ImmutableArray.CreateBuilder(methodDeclarationSyntax.ParameterList.Parameters.Count); + + foreach (var parameterSyntax in methodDeclarationSyntax.ParameterList.Parameters) + { + if (parameterSyntax.Type != null) + { + var expectedParameterTypeSymbol = context.SemanticModel.GetTypeInfo(parameterSyntax.Type).Type; + if (expectedParameterTypeSymbol != null && expectedParameterTypeSymbol.TypeKind != TypeKind.Error) + { + methodParameterTypeSymbolsBuilder.Add(expectedParameterTypeSymbol); + + continue; + } + + methodParameterTypeSymbolsBuilder.Add(null); + } + } + + var methodParameterTypeSymbols = methodParameterTypeSymbolsBuilder.ToImmutable(); + + foreach (var argumentsAttributeSyntax in argumentsAttributes) + { + if (argumentsAttributeSyntax.ArgumentList == null) + { + if (methodDeclarationSyntax.ParameterList.Parameters.Count > 0) + { + ReportMustHaveMatchingValueCountDiagnostic(argumentsAttributeSyntax.GetLocation(), 0); + } + } + else if (!argumentsAttributeSyntax.ArgumentList.Arguments.Any()) + { + if (methodDeclarationSyntax.ParameterList.Parameters.Count > 0) + { + ReportMustHaveMatchingValueCountDiagnostic(argumentsAttributeSyntax.ArgumentList.GetLocation(), 0); + } + } + else + { + // Check if this is an explicit params array creation + + var attributeArgumentSyntax = argumentsAttributeSyntax.ArgumentList.Arguments.First(); + if (attributeArgumentSyntax.NameEquals != null) + { + // Ignore named arguments, e.g. Priority + if (methodDeclarationSyntax.ParameterList.Parameters.Count > 0) + { + ReportMustHaveMatchingValueCountDiagnostic(attributeArgumentSyntax.GetLocation(), 0); + } + } + + // Collection expression + + else if (attributeArgumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax) + { + if (methodDeclarationSyntax.ParameterList.Parameters.Count != collectionExpressionSyntax.Elements.Count) + { + ReportMustHaveMatchingValueCountDiagnostic(collectionExpressionSyntax.Elements.Count == 0 + ? collectionExpressionSyntax.GetLocation() + : Location.Create(context.FilterTree, collectionExpressionSyntax.Elements.Span), + collectionExpressionSyntax.Elements.Count); + + continue; + } + + ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(i => collectionExpressionSyntax.Elements[i] is ExpressionElementSyntax expressionElementSyntax ? expressionElementSyntax.Expression : null); + } + + // Array creation expression + else + { + var attributeArgumentSyntaxValueType = context.SemanticModel.GetTypeInfo(attributeArgumentSyntax.Expression).Type; + if (attributeArgumentSyntaxValueType is IArrayTypeSymbol arrayTypeSymbol && arrayTypeSymbol.ElementType.SpecialType == SpecialType.System_Object) + { + if (attributeArgumentSyntax.Expression is ArrayCreationExpressionSyntax arrayCreationExpressionSyntax) + { + if (arrayCreationExpressionSyntax.Initializer == null) + { + var rankSpecifierSizeSyntax = arrayCreationExpressionSyntax.Type.RankSpecifiers.First().Sizes.First(); + if (rankSpecifierSizeSyntax is LiteralExpressionSyntax literalExpressionSyntax && literalExpressionSyntax.IsKind(SyntaxKind.NumericLiteralExpression)) + { + if (literalExpressionSyntax.Token.Value is 0) + { + if (methodDeclarationSyntax.ParameterList.Parameters.Count > 0) + { + ReportMustHaveMatchingValueCountDiagnostic(literalExpressionSyntax.GetLocation(), 0); + } + } + } + } + else + { + if (methodDeclarationSyntax.ParameterList.Parameters.Count != arrayCreationExpressionSyntax.Initializer.Expressions.Count) + { + ReportMustHaveMatchingValueCountDiagnostic(arrayCreationExpressionSyntax.Initializer.Expressions.Count == 0 + ? arrayCreationExpressionSyntax.Initializer.GetLocation() + : Location.Create(context.FilterTree, arrayCreationExpressionSyntax.Initializer.Expressions.Span), + arrayCreationExpressionSyntax.Initializer.Expressions.Count); + + continue; + } + + // ReSharper disable once PossibleNullReferenceException + ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(i => arrayCreationExpressionSyntax.Initializer.Expressions[i]); + } + } + } + else + { + // Params values + + var firstNamedArgumentIndex = IndexOfNamedArgument(argumentsAttributeSyntax.ArgumentList.Arguments); + if (firstNamedArgumentIndex > 0) + { + if (methodDeclarationSyntax.ParameterList.Parameters.Count != firstNamedArgumentIndex.Value) + { + ReportMustHaveMatchingValueCountDiagnostic(Location.Create(context.FilterTree, TextSpan.FromBounds(argumentsAttributeSyntax.ArgumentList.Arguments.Span.Start, argumentsAttributeSyntax.ArgumentList.Arguments[firstNamedArgumentIndex.Value - 1].Span.End)), + firstNamedArgumentIndex.Value); + + continue; + } + + // ReSharper disable once PossibleNullReferenceException + ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(i => argumentsAttributeSyntax.ArgumentList.Arguments[i].Expression); + } + else + { + if (methodDeclarationSyntax.ParameterList.Parameters.Count != argumentsAttributeSyntax.ArgumentList.Arguments.Count) + { + ReportMustHaveMatchingValueCountDiagnostic(Location.Create(context.FilterTree, argumentsAttributeSyntax.ArgumentList.Arguments.Span), + argumentsAttributeSyntax.ArgumentList.Arguments.Count); + + continue; + } + + // ReSharper disable once PossibleNullReferenceException + ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(i => argumentsAttributeSyntax.ArgumentList.Arguments[i].Expression); + } + } + } + } + } + + return; + + void ReportMustHaveMatchingValueCountDiagnostic(Location diagnosticLocation, int valueCount) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueCountRule, diagnosticLocation, + methodDeclarationSyntax.ParameterList.Parameters.Count, + methodDeclarationSyntax.ParameterList.Parameters.Count == 1 ? "" : "s", + methodDeclarationSyntax.Identifier.ToString(), + valueCount)); + } + + void ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(Func valueExpressionSyntaxFunc) + { + for (var i = 0; i < methodParameterTypeSymbols.Length; i++) + { + var methodParameterTypeSymbol = methodParameterTypeSymbols[i]; + if (methodParameterTypeSymbol == null) + { + continue; + } + + var valueExpressionSyntax = valueExpressionSyntaxFunc(i); + if (valueExpressionSyntax == null) + { + continue; + } + + var valueExpressionString = valueExpressionSyntax.ToString(); + + var constantValue = context.SemanticModel.GetConstantValue(valueExpressionSyntax); + + var actualValueTypeSymbol = context.SemanticModel.GetTypeInfo(valueExpressionSyntax).Type; + if (actualValueTypeSymbol != null && actualValueTypeSymbol.TypeKind != TypeKind.Error) + { + if (!AnalyzerHelper.IsAssignableToLocal(context.Compilation, + methodParameterTypeSymbol, + valueExpressionString, + constantValue, + actualValueTypeSymbol.ToString())) + { + ReportValueTypeMustBeImplicitlyConvertibleDiagnostic(valueExpressionSyntax.GetLocation(), + valueExpressionSyntax.ToString(), + methodParameterTypeSymbol.ToString(), + actualValueTypeSymbol.ToString()); + } + } + else + { + if (constantValue is { HasValue: true, Value: null }) + { + if (!AnalyzerHelper.IsAssignableToLocal(context.Compilation, + methodParameterTypeSymbol, + valueExpressionString, + constantValue, + null)) + { + ReportValueTypeMustBeImplicitlyConvertibleDiagnostic(valueExpressionSyntax.GetLocation(), + valueExpressionString, + methodParameterTypeSymbol.ToString(), + "null"); + } + } + } + } + + return; + + void ReportValueTypeMustBeImplicitlyConvertibleDiagnostic(Location diagnosticLocation, string value, string expectedType, string actualType) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueTypeRule, + diagnosticLocation, + value, + expectedType, + actualType)); + } + } + } + + private static int? IndexOfNamedArgument(SeparatedSyntaxList attributeArguments) + { + var i = 0; + + foreach (var attributeArgumentSyntax in attributeArguments) + { + if (attributeArgumentSyntax.NameEquals != null) + { + return i; + } + + i++; + } + + return null; + } + } +} diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs new file mode 100644 index 0000000000..c174280c20 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs @@ -0,0 +1,333 @@ +namespace BenchmarkDotNet.Analyzers.Attributes +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor MutuallyExclusiveOnFieldRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Description))); + + internal static readonly DiagnosticDescriptor MutuallyExclusiveOnPropertyRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Description))); + + internal static readonly DiagnosticDescriptor FieldMustBePublic = new DiagnosticDescriptor(DiagnosticIds.Attributes_GeneralParameterAttributes_FieldMustBePublic, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_FieldMustBePublic_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_FieldMustBePublic_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_FieldMustBePublic_Description))); + + internal static readonly DiagnosticDescriptor PropertyMustBePublic = new DiagnosticDescriptor(DiagnosticIds.Attributes_GeneralParameterAttributes_PropertyMustBePublic, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyMustBePublic_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyMustBePublic_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyMustBePublic_Description))); + + internal static readonly DiagnosticDescriptor NotValidOnReadonlyFieldRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_GeneralParameterAttributes_NotValidOnReadonlyField, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Description))); + + internal static readonly DiagnosticDescriptor NotValidOnConstantFieldRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_GeneralParameterAttributes_NotValidOnConstantField, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_NotValidOnConstantField_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_NotValidOnConstantField_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor PropertyMustHavePublicSetterRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description))); + + internal static readonly DiagnosticDescriptor PropertyCannotBeInitOnlyRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description))); + + public override ImmutableArray SupportedDiagnostics => + [ + MutuallyExclusiveOnFieldRule, + MutuallyExclusiveOnPropertyRule, + FieldMustBePublic, + PropertyMustBePublic, + NotValidOnReadonlyFieldRule, + NotValidOnConstantFieldRule, + PropertyCannotBeInitOnlyRule, + PropertyMustHavePublicSetterRule + ]; + + public override void Initialize(AnalysisContext analysisContext) + { + analysisContext.EnableConcurrentExecution(); + analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + + analysisContext.RegisterCompilationStartAction(ctx => + { + // Only run if BenchmarkDotNet.Annotations is referenced + var benchmarkAttributeTypeSymbol = AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(ctx.Compilation); + if (benchmarkAttributeTypeSymbol == null) + { + return; + } + + ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.Attribute); + }); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + if (context.Node is not AttributeSyntax attributeSyntax) + { + return; + } + + if (!AllAttributeTypeSymbolsExist(context, out var paramsAttributeTypeSymbol, out var paramsSourceAttributeTypeSymbol, out var paramsAllValuesAttributeTypeSymbol)) + { + return; + } + + var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; + if ( attributeSyntaxTypeSymbol == null + || attributeSyntaxTypeSymbol.TypeKind == TypeKind.Error + || + (!attributeSyntaxTypeSymbol.Equals(paramsAttributeTypeSymbol, SymbolEqualityComparer.Default) + && !attributeSyntaxTypeSymbol.Equals(paramsSourceAttributeTypeSymbol, SymbolEqualityComparer.Default) + && !attributeSyntaxTypeSymbol.Equals(paramsAllValuesAttributeTypeSymbol, SymbolEqualityComparer.Default))) + { + return; + } + + var attributeTarget = attributeSyntax.FirstAncestorOrSelf(n => n is FieldDeclarationSyntax or PropertyDeclarationSyntax); + if (attributeTarget == null) + { + return; + } + + ImmutableArray declaredAttributes; + bool fieldOrPropertyIsPublic; + Location fieldConstModifierLocation = null; + Location fieldReadonlyModifierLocation = null; + string fieldOrPropertyIdentifier; + Location propertyInitAccessorKeywordLocation = null; + Location fieldOrPropertyIdentifierLocation; + bool propertyIsMissingAssignableSetter = false; + DiagnosticDescriptor fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule; + DiagnosticDescriptor fieldOrPropertyMustBePublicDiagnosticRule; + + if (attributeTarget is FieldDeclarationSyntax fieldDeclarationSyntax) + { + declaredAttributes = fieldDeclarationSyntax.AttributeLists.SelectMany(als => als.Attributes).ToImmutableArray(); + fieldOrPropertyIsPublic = fieldDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword); + + var fieldConstModifierIndex = fieldDeclarationSyntax.Modifiers.IndexOf(SyntaxKind.ConstKeyword); + fieldConstModifierLocation = fieldConstModifierIndex >= 0 ? fieldDeclarationSyntax.Modifiers[fieldConstModifierIndex].GetLocation() : null; + + var fieldOrPropertyReadonlyModifierIndex = fieldDeclarationSyntax.Modifiers.IndexOf(SyntaxKind.ReadOnlyKeyword); + fieldReadonlyModifierLocation = fieldOrPropertyReadonlyModifierIndex >= 0 ? fieldDeclarationSyntax.Modifiers[fieldOrPropertyReadonlyModifierIndex].GetLocation() : null; + + fieldOrPropertyIdentifier = fieldDeclarationSyntax.Declaration.Variables[0].Identifier.ToString(); + fieldOrPropertyIdentifierLocation = fieldDeclarationSyntax.Declaration.Variables[0].Identifier.GetLocation(); + fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule = MutuallyExclusiveOnFieldRule; + fieldOrPropertyMustBePublicDiagnosticRule = FieldMustBePublic; + } + else if (attributeTarget is PropertyDeclarationSyntax propertyDeclarationSyntax) + { + declaredAttributes = propertyDeclarationSyntax.AttributeLists.SelectMany(als => als.Attributes).ToImmutableArray(); + fieldOrPropertyIsPublic = propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword); + fieldOrPropertyIdentifier = propertyDeclarationSyntax.Identifier.ToString(); + + var propertyInitAccessorIndex = propertyDeclarationSyntax.AccessorList?.Accessors.IndexOf(SyntaxKind.InitAccessorDeclaration); + propertyInitAccessorKeywordLocation = propertyInitAccessorIndex >= 0 ? propertyDeclarationSyntax.AccessorList.Accessors[propertyInitAccessorIndex.Value].Keyword.GetLocation() : null; + + var propertySetAccessorIndex = propertyDeclarationSyntax.AccessorList?.Accessors.IndexOf(SyntaxKind.SetAccessorDeclaration); + propertyIsMissingAssignableSetter = !propertySetAccessorIndex.HasValue || propertySetAccessorIndex.Value < 0 || propertyDeclarationSyntax.AccessorList.Accessors[propertySetAccessorIndex.Value].Modifiers.Any(); + + fieldOrPropertyIdentifierLocation = propertyDeclarationSyntax.Identifier.GetLocation(); + fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule = MutuallyExclusiveOnPropertyRule; + fieldOrPropertyMustBePublicDiagnosticRule = PropertyMustBePublic; + } + else + { + return; + } + + AnalyzeFieldOrPropertySymbol(context, + paramsAttributeTypeSymbol, + paramsSourceAttributeTypeSymbol, + paramsAllValuesAttributeTypeSymbol, + declaredAttributes, + fieldOrPropertyIsPublic, + fieldConstModifierLocation, + fieldReadonlyModifierLocation, + fieldOrPropertyIdentifier, + propertyInitAccessorKeywordLocation, + propertyIsMissingAssignableSetter, + fieldOrPropertyIdentifierLocation, + fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule, + fieldOrPropertyMustBePublicDiagnosticRule, + attributeSyntax); + } + + private static void AnalyzeFieldOrPropertySymbol(SyntaxNodeAnalysisContext context, + INamedTypeSymbol? paramsAttributeTypeSymbol, + INamedTypeSymbol? paramsSourceAttributeTypeSymbol, + INamedTypeSymbol? paramsAllValuesAttributeTypeSymbol, + ImmutableArray declaredAttributes, + bool fieldOrPropertyIsPublic, + Location? fieldConstModifierLocation, + Location? fieldReadonlyModifierLocation, + string fieldOrPropertyIdentifier, + Location? propertyInitAccessorKeywordLocation, + bool propertyIsMissingAssignableSetter, + Location fieldOrPropertyIdentifierLocation, + DiagnosticDescriptor fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule, + DiagnosticDescriptor fieldOrPropertyMustBePublicDiagnosticRule, + AttributeSyntax attributeSyntax) + { + var applicableParameterAttributeTypeSymbols = new[] + { + paramsAttributeTypeSymbol, + paramsSourceAttributeTypeSymbol, + paramsAllValuesAttributeTypeSymbol + }.ToImmutableArray(); + + var parameterAttributeTypeSymbols = new HashSet(SymbolEqualityComparer.Default); + + foreach (var declaredAttributeSyntax in declaredAttributes) + { + var declaredAttributeTypeSymbol = context.SemanticModel.GetTypeInfo(declaredAttributeSyntax).Type; + if (declaredAttributeTypeSymbol != null) + { + foreach (var applicableParameterAttributeTypeSymbol in applicableParameterAttributeTypeSymbols) + { + if (declaredAttributeTypeSymbol.Equals(applicableParameterAttributeTypeSymbol, SymbolEqualityComparer.Default)) + { + if (!parameterAttributeTypeSymbols.Add(applicableParameterAttributeTypeSymbol)) + { + return; + } + } + } + } + } + + if (parameterAttributeTypeSymbols.Count == 0) + { + return; + } + + if (parameterAttributeTypeSymbols.Count == 1) + { + if (fieldConstModifierLocation != null) + { + context.ReportDiagnostic(Diagnostic.Create(NotValidOnConstantFieldRule, + fieldConstModifierLocation, + fieldOrPropertyIdentifier, + attributeSyntax.Name.ToString())); + + return; + } + + if (!fieldOrPropertyIsPublic) + { + context.ReportDiagnostic(Diagnostic.Create(fieldOrPropertyMustBePublicDiagnosticRule, + fieldOrPropertyIdentifierLocation, + fieldOrPropertyIdentifier, + attributeSyntax.Name.ToString())); + } + + if (fieldReadonlyModifierLocation != null) + { + context.ReportDiagnostic(Diagnostic.Create(NotValidOnReadonlyFieldRule, + fieldReadonlyModifierLocation, + fieldOrPropertyIdentifier, + attributeSyntax.Name.ToString())); + } + + if (propertyInitAccessorKeywordLocation != null) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyCannotBeInitOnlyRule, + propertyInitAccessorKeywordLocation, + fieldOrPropertyIdentifier, + attributeSyntax.Name.ToString())); + } + else if (propertyIsMissingAssignableSetter) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyMustHavePublicSetterRule, + fieldOrPropertyIdentifierLocation, + fieldOrPropertyIdentifier, + attributeSyntax.Name.ToString())); + } + + return; + } + + context.ReportDiagnostic(Diagnostic.Create(fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule, + attributeSyntax.GetLocation(), + fieldOrPropertyIdentifier)); + } + + private static bool AllAttributeTypeSymbolsExist(in SyntaxNodeAnalysisContext context, + out INamedTypeSymbol? paramsAttributeTypeSymbol, + out INamedTypeSymbol? paramsSourceAttributeTypeSymbol, + out INamedTypeSymbol? paramsAllValuesAttributeTypeSymbol) + { + paramsAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsAttribute"); + if (paramsAttributeTypeSymbol == null) + { + paramsSourceAttributeTypeSymbol = null; + paramsAllValuesAttributeTypeSymbol = null; + + return false; + } + + paramsSourceAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsSourceAttribute"); + if (paramsSourceAttributeTypeSymbol == null) + { + paramsAllValuesAttributeTypeSymbol = null; + + return false; + } + + paramsAllValuesAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsAllValuesAttribute"); + if (paramsAllValuesAttributeTypeSymbol == null) + { + return false; + } + + return true; + } + } +} diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs new file mode 100644 index 0000000000..30a8a7b1f3 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs @@ -0,0 +1,129 @@ +namespace BenchmarkDotNet.Analyzers.Attributes +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + using System.Collections.Immutable; + using System.Linq; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ParamsAllValuesAttributeAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor NotAllowedOnFlagsEnumPropertyOrFieldTypeRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Description))); + + internal static readonly DiagnosticDescriptor PropertyOrFieldTypeMustBeEnumOrBoolRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => + [ + NotAllowedOnFlagsEnumPropertyOrFieldTypeRule, + PropertyOrFieldTypeMustBeEnumOrBoolRule + ]; + + public override void Initialize(AnalysisContext analysisContext) + { + analysisContext.EnableConcurrentExecution(); + analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + analysisContext.RegisterCompilationStartAction(ctx => + { + // Only run if BenchmarkDotNet.Annotations is referenced + if (GetParamsAllValuesAttributeTypeSymbol(ctx.Compilation) == null) + { + return; + } + + ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.Attribute); + }); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + if (context.Node is not AttributeSyntax attributeSyntax) + { + return; + } + + var paramsAllValuesAttributeTypeSymbol = GetParamsAllValuesAttributeTypeSymbol(context.Compilation); + + var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; + if (attributeSyntaxTypeSymbol == null || !attributeSyntaxTypeSymbol.Equals(paramsAllValuesAttributeTypeSymbol, SymbolEqualityComparer.Default)) + { + return; + } + + var attributeTarget = attributeSyntax.FirstAncestorOrSelf(n => n is FieldDeclarationSyntax or PropertyDeclarationSyntax); + if (attributeTarget == null) + { + return; + } + + TypeSyntax fieldOrPropertyTypeSyntax; + + if (attributeTarget is FieldDeclarationSyntax fieldDeclarationSyntax) + { + fieldOrPropertyTypeSyntax = fieldDeclarationSyntax.Declaration.Type; + + } + else if (attributeTarget is PropertyDeclarationSyntax propertyDeclarationSyntax) + { + fieldOrPropertyTypeSyntax = propertyDeclarationSyntax.Type; + } + else + { + return; + } + + AnalyzeFieldOrPropertyTypeSyntax(context, fieldOrPropertyTypeSyntax); + } + + private static void AnalyzeFieldOrPropertyTypeSyntax(SyntaxNodeAnalysisContext context, TypeSyntax fieldOrPropertyTypeSyntax) + { + if (fieldOrPropertyTypeSyntax is NullableTypeSyntax fieldOrPropertyNullableTypeSyntax) + { + fieldOrPropertyTypeSyntax = fieldOrPropertyNullableTypeSyntax.ElementType; + } + + var fieldOrPropertyTypeSymbol = context.SemanticModel.GetTypeInfo(fieldOrPropertyTypeSyntax).Type; + if (fieldOrPropertyTypeSymbol == null || fieldOrPropertyTypeSymbol.TypeKind == TypeKind.Error) + { + return; + } + + if (fieldOrPropertyTypeSymbol.TypeKind == TypeKind.Enum) + { + var flagsAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("System.FlagsAttribute"); + if (flagsAttributeTypeSymbol == null) + { + return; + } + + if (fieldOrPropertyTypeSymbol.GetAttributes().Any(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(flagsAttributeTypeSymbol, SymbolEqualityComparer.Default))) + { + context.ReportDiagnostic(Diagnostic.Create(NotAllowedOnFlagsEnumPropertyOrFieldTypeRule, fieldOrPropertyTypeSyntax.GetLocation(), fieldOrPropertyTypeSymbol.ToString())); + } + + return; + } + + if (fieldOrPropertyTypeSymbol.SpecialType != SpecialType.System_Boolean) + { + context.ReportDiagnostic(Diagnostic.Create(PropertyOrFieldTypeMustBeEnumOrBoolRule, fieldOrPropertyTypeSyntax.GetLocation())); + } + } + + private static INamedTypeSymbol? GetParamsAllValuesAttributeTypeSymbol(Compilation compilation) => compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsAllValuesAttribute"); + } +} diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs new file mode 100644 index 0000000000..126d2daf1f --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs @@ -0,0 +1,291 @@ +namespace BenchmarkDotNet.Analyzers.Attributes +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + using System.Collections.Immutable; + using System.Linq; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ParamsAttributeAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor MustHaveValuesRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ParamsAttribute_MustHaveValues, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAttribute_MustHaveValues_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAttribute_MustHaveValues_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor MustHaveMatchingValueTypeRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ParamsAttribute_MustHaveMatchingValueType, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAttribute_MustHaveMatchingValueType_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAttribute_MustHaveMatchingValueType_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAttribute_MustHaveMatchingValueType_Description))); + + internal static readonly DiagnosticDescriptor UnnecessarySingleValuePassedToAttributeRule = new DiagnosticDescriptor(DiagnosticIds.Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_MessageFormat)), + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + MustHaveValuesRule, + MustHaveMatchingValueTypeRule, + UnnecessarySingleValuePassedToAttributeRule + ); + + public override void Initialize(AnalysisContext analysisContext) + { + analysisContext.EnableConcurrentExecution(); + analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + + analysisContext.RegisterCompilationStartAction(ctx => + { + // Only run if BenchmarkDotNet.Annotations is referenced + if (GetParamsAttributeTypeSymbol(ctx.Compilation) == null) + { + return; + } + + ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.Attribute); + }); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + if (context.Node is not AttributeSyntax attributeSyntax) + { + return; + } + + var paramsAttributeTypeSymbol = GetParamsAttributeTypeSymbol(context.Compilation); + + var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; + if (attributeSyntaxTypeSymbol == null || !attributeSyntaxTypeSymbol.Equals(paramsAttributeTypeSymbol, SymbolEqualityComparer.Default)) + { + return; + } + + var attributeTarget = attributeSyntax.FirstAncestorOrSelf(n => n is FieldDeclarationSyntax or PropertyDeclarationSyntax); + if (attributeTarget == null) + { + return; + } + + TypeSyntax fieldOrPropertyTypeSyntax; + + if (attributeTarget is FieldDeclarationSyntax fieldDeclarationSyntax) + { + fieldOrPropertyTypeSyntax = fieldDeclarationSyntax.Declaration.Type; + + } + else if (attributeTarget is PropertyDeclarationSyntax propertyDeclarationSyntax) + { + fieldOrPropertyTypeSyntax = propertyDeclarationSyntax.Type; + } + else + { + return; + } + + AnalyzeFieldOrPropertyTypeSyntax(context, + fieldOrPropertyTypeSyntax, + attributeSyntax); + } + + private static void AnalyzeFieldOrPropertyTypeSyntax(SyntaxNodeAnalysisContext context, + TypeSyntax fieldOrPropertyTypeSyntax, + AttributeSyntax attributeSyntax) + { + if (attributeSyntax.ArgumentList == null) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, + attributeSyntax.GetLocation())); + + return; + } + + if (!attributeSyntax.ArgumentList.Arguments.Any()) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, + attributeSyntax.ArgumentList.GetLocation())); + + return; + } + + if (attributeSyntax.ArgumentList.Arguments.All(aas => aas.NameEquals != null)) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, + Location.Create(context.FilterTree, attributeSyntax.ArgumentList.Arguments.Span))); + + return; + } + + var expectedValueTypeSymbol = context.SemanticModel.GetTypeInfo(fieldOrPropertyTypeSyntax).Type; + if (expectedValueTypeSymbol == null || expectedValueTypeSymbol.TypeKind == TypeKind.Error) + { + return; + } + + + // Check if this is an explicit params array creation + + var attributeArgumentSyntax = attributeSyntax.ArgumentList.Arguments.First(); + if (attributeArgumentSyntax.NameEquals != null) + { + // Ignore named arguments, e.g. Priority + return; + } + + // Collection expression + + if (attributeArgumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax) + { + if (!collectionExpressionSyntax.Elements.Any()) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, + collectionExpressionSyntax.GetLocation())); + return; + } + + if (collectionExpressionSyntax.Elements.Count == 1) + { + context.ReportDiagnostic(Diagnostic.Create(UnnecessarySingleValuePassedToAttributeRule, + collectionExpressionSyntax.Elements[0].GetLocation())); + } + + foreach (var collectionElementSyntax in collectionExpressionSyntax.Elements) + { + if (collectionElementSyntax is ExpressionElementSyntax expressionElementSyntax) + { + ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(expressionElementSyntax.Expression); + } + } + + return; + } + + // Array creation expression + + var attributeArgumentSyntaxValueType = context.SemanticModel.GetTypeInfo(attributeArgumentSyntax.Expression).Type; + if (attributeArgumentSyntaxValueType is IArrayTypeSymbol arrayTypeSymbol && arrayTypeSymbol.ElementType.SpecialType == SpecialType.System_Object) + { + if (attributeArgumentSyntax.Expression is ArrayCreationExpressionSyntax arrayCreationExpressionSyntax) + { + if (arrayCreationExpressionSyntax.Initializer == null) + { + var rankSpecifierSizeSyntax = arrayCreationExpressionSyntax.Type.RankSpecifiers.First().Sizes.First(); + if (rankSpecifierSizeSyntax is LiteralExpressionSyntax literalExpressionSyntax && literalExpressionSyntax.IsKind(SyntaxKind.NumericLiteralExpression)) + { + if (literalExpressionSyntax.Token.Value is 0) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, + arrayCreationExpressionSyntax.GetLocation())); + } + } + + return; + } + + if (!arrayCreationExpressionSyntax.Initializer.Expressions.Any()) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, + arrayCreationExpressionSyntax.Initializer.GetLocation())); + + return; + } + + if (arrayCreationExpressionSyntax.Initializer.Expressions.Count == 1) + { + context.ReportDiagnostic(Diagnostic.Create(UnnecessarySingleValuePassedToAttributeRule, + arrayCreationExpressionSyntax.Initializer.Expressions[0].GetLocation())); + } + + foreach (var expressionSyntax in arrayCreationExpressionSyntax.Initializer.Expressions) + { + ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(expressionSyntax); + } + } + + return; + } + + + // Params values + + if (attributeSyntax.ArgumentList.Arguments.Count(aas => aas.NameEquals == null) == 1) + { + context.ReportDiagnostic(Diagnostic.Create(UnnecessarySingleValuePassedToAttributeRule, + attributeArgumentSyntax.Expression.GetLocation())); + } + + foreach (var parameterValueAttributeArgumentSyntax in attributeSyntax.ArgumentList.Arguments) + { + if (parameterValueAttributeArgumentSyntax.NameEquals != null) + { + // Ignore named arguments, e.g. Priority + continue; + } + + ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(parameterValueAttributeArgumentSyntax.Expression); + } + + return; + + void ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(ExpressionSyntax valueExpressionSyntax) + { + var constantValue = context.SemanticModel.GetConstantValue(valueExpressionSyntax); + + var valueExpressionString = valueExpressionSyntax.ToString(); + + var actualValueTypeSymbol = context.SemanticModel.GetTypeInfo(valueExpressionSyntax).Type; + if (actualValueTypeSymbol != null && actualValueTypeSymbol.TypeKind != TypeKind.Error) + { + if (!AnalyzerHelper.IsAssignableToField(context.Compilation, + expectedValueTypeSymbol, + valueExpressionString, + constantValue, + actualValueTypeSymbol.ToString())) + { + ReportValueTypeMustBeImplicitlyConvertibleDiagnostic(valueExpressionSyntax.GetLocation(), + valueExpressionString, + fieldOrPropertyTypeSyntax.ToString(), + actualValueTypeSymbol.ToString()); + } + } + else + { + if (constantValue is { HasValue: true, Value: null }) + { + if (!AnalyzerHelper.IsAssignableToField(context.Compilation, expectedValueTypeSymbol, valueExpressionString, constantValue, null)) + { + ReportValueTypeMustBeImplicitlyConvertibleDiagnostic(valueExpressionSyntax.GetLocation(), + valueExpressionString, + fieldOrPropertyTypeSyntax.ToString(), + "null"); + } + } + } + + return; + + void ReportValueTypeMustBeImplicitlyConvertibleDiagnostic(Location diagnosticLocation, string value, string expectedType, string actualType) + { + context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueTypeRule, + diagnosticLocation, + value, + expectedType, + actualType)); + } + } + } + + private static INamedTypeSymbol? GetParamsAttributeTypeSymbol(Compilation compilation) => compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsAttribute"); + } +} diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj new file mode 100644 index 0000000000..c1751c49c0 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj @@ -0,0 +1,40 @@ + + + + netstandard2.0 + false + + + *$(MSBuildProjectFile)* + true + + + + + + + + + + + + + + True + True + BenchmarkDotNetAnalyzerResources.resx + + + ResXFileCodeGenerator + BenchmarkDotNetAnalyzerResources.Designer.cs + + + + + + <_Parameter1>BenchmarkDotNet.Analyzers.Tests,PublicKey=00240000048000009400000006020000002400005253413100040000010001002970bbdfca4d129fc74b4845b239973f1b183684f0d7db5e1de7e085917e3656cf94884803cb800d85d5aae5838fb3f8fd1f2829e8208c4f087afcfe970bce44037ba30a66749cd5514b410ca8a35e9c7d6eb86975853c834c9ad25051537f9a05a0c540c5d84f2c7b32ab01619d84367fd424797ba3242f08b0e6ae75f66dad + + + + + diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs new file mode 100644 index 0000000000..e5ffd7c897 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs @@ -0,0 +1,801 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BenchmarkDotNet.Analyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class BenchmarkDotNetAnalyzerResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal BenchmarkDotNetAnalyzerResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BenchmarkDotNet.Analyzers.BenchmarkDotNetAnalyzerResources", typeof(BenchmarkDotNetAnalyzerResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to This method declares one or more parameters but is not annotated with any [Arguments] attributes. To ensure correct argument binding, methods with parameters must explicitly be annotated with one or more [Arguments] attributes. + ///Either add the [Arguments] attribute(s) or remove the parameters.. + /// + internal static string Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_Description { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_Descript" + + "ion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark method '{0}' without [Arguments] attribute(s) cannot declare parameters. + /// + internal static string Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_MessageF" + + "ormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark methods without [Arguments] attribute(s) cannot declare parameters. + /// + internal static string Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Description { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expected {0} value{1} as declared by the benchmark method '{2}', but found {3}. Update the attribute usage or method to match.. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The values passed to an [Arguments] attribute must match the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected type for argument value '{0}'. Expected '{1}' but found '{2}'.. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Values passed to an [Arguments] attribute must match exactly the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [Arguments] attribute can only be used on methods annotated with the [Benchmark] attribute. + /// + internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Arguments] attribute can only be used on methods annotated with the [Benchmark] attribute. + /// + internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A field annotated with a parameter attribute must be public. + /// + internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field '{0}' annotated with [{1}] must be public. + /// + internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fields annotated with a parameter attribute must be public. + /// + internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a field at any one time. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Duplicate parameter attribute on field '{0}'. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one parameter attribute can be applied to a field. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a property at any one time. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Duplicate parameter attribute on property '{0}'. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one parameter attribute can be applied to a property. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attribute [{0}] is not valid on constants. It is only valid on non-constant field declarations.. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnConstantField_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnConstantField_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attributes are not valid on constant field declarations. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnConstantField_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnConstantField_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attributes are not valid on fields with a readonly modifier. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Modifier 'readonly' is not valid on field '{0}' annotated with parameter attribute [{1}]. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fields annotated with a parameter attribute cannot be read-only. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A property annotated with a parameter attribute must have a public, assignable setter i.e. { set; }. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' annotated with [{1}] cannot be init-only. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties annotated with a parameter attribute cannot have an init-only setter. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A property annotated with a parameter attribute must be public. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must be public. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties annotated with a parameter attribute must be public. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A property annotated with a parameter attribute must have a public setter; make sure that the access modifier of the setter is empty and that the property is not an auto-property or an expression-bodied property.. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must have a public setter. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties annotated with a parameter attribute must have a public setter. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [ParamsAllValues] attribute cannot be applied to a field or property of an enum type marked with the [Flags] attribute. Use this attribute only with non-flags enum types, as [Flags] enums support bitwise combinations that cannot be exhaustively enumerated.. + /// + internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Description { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Desc" + + "ription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field or property enum type '{0}' is marked with [Flags] and cannot be used with this attribute. + /// + internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Mess" + + "ageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [ParamsAllValues] attribute cannot be applied to fields or properties of enum types marked with [Flags]. + /// + internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Titl" + + "e", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [ParamsAllValues] attribute can only be applied to a field or property of enum or bool type (or nullable of these types). + /// + internal static string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_MessageFo" + + "rmat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [ParamsAllValues] attribute is only valid on fields or properties of enum or bool type and nullable type for another allowed type. + /// + internal static string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type of each value provided to the [Params] attribute must match the type of (or be implicitly convertible to) the field or property it is applied to. + /// + internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_Description { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected type for parameter value '{0}'. Expected '{1}' but found '{2}'.. + /// + internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type of all value(s) passed to the [Params] attribute must match the type of (or be implicitly convertible to) the annotated field or property. + /// + internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [Params] attribute requires at least one value. No values were provided, or an empty array was specified.. + /// + internal static string Attributes_ParamsAttribute_MustHaveValues_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveValues_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [Params] attribute must include at least one value. + /// + internal static string Attributes_ParamsAttribute_MustHaveValues_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveValues_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Providing a single value to the [Params] attribute is unnecessary. This attribute is only useful when provided two or more values.. + /// + internal static string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unnecessary single value passed to [Params] attribute. + /// + internal static string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A generic benchmark class referenced in the BenchmarkRunner.Run method must be annotated with at least one [GenericTypeArguments] attribute. + /// + internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Description { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + + "ntsAttribute_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Referenced generic benchmark class '{0}' has no [GenericTypeArguments] attribute(s). + /// + internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + + "ntsAttribute_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generic benchmark classes must be annotated with at least one [GenericTypeArguments] attribute. + /// + internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + + "ntsAttribute_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The referenced benchmark class (or any of its inherited classes) must have at least one method annotated with the [Benchmark] attribute. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Description { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Intended benchmark class '{0}' (or any of its ancestors) has no method(s) annotated with the [Benchmark] attribute. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark class (or any of its ancestors) has no annotated method(s). + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class referenced in the BenchmarkRunner.Run method must be non-abstract. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Description { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Referenced benchmark class '{0}' cannot be abstract. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes must be non-abstract. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Referenced benchmark class '{0}' must be public. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes must be public. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBePublic_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class referenced in the BenchmarkRunner.Run method must be unsealed. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Description { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Referenced benchmark class '{0}' is sealed. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes must be unsealed. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class must be an instance class. + /// + internal static string General_BenchmarkClass_ClassMustBeNonStatic_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark class '{0}' cannot be static. + /// + internal static string General_BenchmarkClass_ClassMustBeNonStatic_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes must be non-static. + /// + internal static string General_BenchmarkClass_ClassMustBeNonStatic_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class annotated with a [GenericTypeArguments] attribute must be generic, having between one to three type parameters. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Descri" + + "ption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark class '{0}' must be generic. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Messag" + + "eFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes annotated with a [GenericTypeArguments] attribute must be generic. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class annotated with a [GenericTypeArguments] attribute must be non-abstract. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_De" + + "scription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark class '{0}' cannot be abstract. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_Me" + + "ssageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes annotated with the [GenericTypeArguments] attribute must be non-abstract. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_Ti" + + "tle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class. + /// + internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + + "Count_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expected {0} type argument{1} as declared on the benchmark class '{2}', but found {3}. Update the attribute usage or the type parameter list of the class declaration to match.. + /// + internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + + "Count_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class. + /// + internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + + "Count_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A method annotated with the [Benchmark] attribute must be non-generic. + /// + internal static string General_BenchmarkClass_MethodMustBeNonGeneric_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The benchmark method '{0}' must be non-generic. + /// + internal static string General_BenchmarkClass_MethodMustBeNonGeneric_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark methods must be non-generic. + /// + internal static string General_BenchmarkClass_MethodMustBeNonGeneric_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A method annotated with the [Benchmark] attribute must be public. + /// + internal static string General_BenchmarkClass_MethodMustBePublic_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The benchmark method '{0}' must be public. + /// + internal static string General_BenchmarkClass_MethodMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark methods must be public. + /// + internal static string General_BenchmarkClass_MethodMustBePublic_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one benchmark method can be marked as baseline. + /// + internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaseline_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaseline_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one benchmark method can be baseline. + /// + internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaseline_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaseline_Title", resourceCulture); + } + } + } +} diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx new file mode 100644 index 0000000000..6fc4189093 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The referenced benchmark class (or any of its inherited classes) must have at least one method annotated with the [Benchmark] attribute + + + Intended benchmark class '{0}' (or any of its ancestors) has no method(s) annotated with the [Benchmark] attribute + + + Referenced benchmark class '{0}' cannot be abstract + + + Benchmark class (or any of its ancestors) has no annotated method(s) + + + Benchmark classes must be non-abstract + + + A benchmark class must be an instance class + + + A benchmark class annotated with a [GenericTypeArguments] attribute must be non-abstract + + + Benchmark class '{0}' cannot be static + + + Benchmark class '{0}' cannot be abstract + + + Referenced generic benchmark class '{0}' has no [GenericTypeArguments] attribute(s) + + + A generic benchmark class referenced in the BenchmarkRunner.Run method must be annotated with at least one [GenericTypeArguments] attribute + + + Benchmark classes must be non-static + + + Benchmark classes annotated with the [GenericTypeArguments] attribute must be non-abstract + + + Generic benchmark classes must be annotated with at least one [GenericTypeArguments] attribute + + + Benchmark classes must be public + + + A benchmark class referenced in the BenchmarkRunner.Run method must be unsealed + + + Referenced benchmark class '{0}' is sealed + + + Benchmark classes must be unsealed + + + A method annotated with the [Benchmark] attribute must be public + + + A method annotated with the [Benchmark] attribute must be non-generic + + + The number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class + + + A benchmark class annotated with a [GenericTypeArguments] attribute must be generic, having between one to three type parameters + + + The benchmark method '{0}' must be public + + + The benchmark method '{0}' must be non-generic + + + Expected {0} type argument{1} as declared on the benchmark class '{2}', but found {3}. Update the attribute usage or the type parameter list of the class declaration to match. + + + Benchmark class '{0}' must be generic + + + Only one benchmark method can be marked as baseline + + + Benchmark methods must be public + + + Benchmark methods must be non-generic + + + Number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class + + + Benchmark classes annotated with a [GenericTypeArguments] attribute must be generic + + + Only one benchmark method can be baseline + + + Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a field at any one time + + + A field annotated with a parameter attribute must be public + + + A property annotated with a parameter attribute must be public + + + A property annotated with a parameter attribute must have a public setter; make sure that the access modifier of the setter is empty and that the property is not an auto-property or an expression-bodied property. + + + The type of each value provided to the [Params] attribute must match the type of (or be implicitly convertible to) the field or property it is applied to + + + The [ParamsAllValues] attribute cannot be applied to a field or property of an enum type marked with the [Flags] attribute. Use this attribute only with non-flags enum types, as [Flags] enums support bitwise combinations that cannot be exhaustively enumerated. + + + A property annotated with a parameter attribute must have a public, assignable setter i.e. { set; } + + + Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a property at any one time + + + Duplicate parameter attribute on field '{0}' + + + Field '{0}' annotated with [{1}] must be public + + + Expected {0} value{1} as declared by the benchmark method '{2}', but found {3}. Update the attribute usage or method to match. + + + Benchmark method '{0}' without [Arguments] attribute(s) cannot declare parameters + + + Unexpected type for argument value '{0}'. Expected '{1}' but found '{2}'. + + + Property '{0}' annotated with [{1}] must be public + + + Property '{0}' annotated with [{1}] must have a public setter + + + The [Params] attribute requires at least one value. No values were provided, or an empty array was specified. + + + Providing a single value to the [Params] attribute is unnecessary. This attribute is only useful when provided two or more values. + + + Unexpected type for parameter value '{0}'. Expected '{1}' but found '{2}'. + + + Field or property enum type '{0}' is marked with [Flags] and cannot be used with this attribute + + + The [ParamsAllValues] attribute can only be applied to a field or property of enum or bool type (or nullable of these types) + + + Property '{0}' annotated with [{1}] cannot be init-only + + + Duplicate parameter attribute on property '{0}' + + + Only one parameter attribute can be applied to a field + + + Fields annotated with a parameter attribute must be public + + + Number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method + + + Benchmark methods without [Arguments] attribute(s) cannot declare parameters + + + Values passed to an [Arguments] attribute must match exactly the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order + + + Properties annotated with a parameter attribute must be public + + + Properties annotated with a parameter attribute must have a public setter + + + The [Params] attribute must include at least one value + + + Unnecessary single value passed to [Params] attribute + + + Type of all value(s) passed to the [Params] attribute must match the type of (or be implicitly convertible to) the annotated field or property + + + The [ParamsAllValues] attribute cannot be applied to fields or properties of enum types marked with [Flags] + + + The [ParamsAllValues] attribute is only valid on fields or properties of enum or bool type and nullable type for another allowed type + + + Properties annotated with a parameter attribute cannot have an init-only setter + + + Only one parameter attribute can be applied to a property + + + Parameter attributes are not valid on fields with a readonly modifier + + + Fields annotated with a parameter attribute cannot be read-only + + + Parameter attributes are not valid on constant field declarations + + + Modifier 'readonly' is not valid on field '{0}' annotated with parameter attribute [{1}] + + + Parameter attribute [{0}] is not valid on constants. It is only valid on non-constant field declarations. + + + The number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method + + + This method declares one or more parameters but is not annotated with any [Arguments] attributes. To ensure correct argument binding, methods with parameters must explicitly be annotated with one or more [Arguments] attributes. +Either add the [Arguments] attribute(s) or remove the parameters. + + + The values passed to an [Arguments] attribute must match the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order + + + [Arguments] attribute can only be used on methods annotated with the [Benchmark] attribute + + + The [Arguments] attribute can only be used on methods annotated with the [Benchmark] attribute + + + A benchmark class referenced in the BenchmarkRunner.Run method must be non-abstract + + + Referenced benchmark class '{0}' must be public + + \ No newline at end of file diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs new file mode 100644 index 0000000000..87dbaa65c9 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs @@ -0,0 +1,222 @@ +namespace BenchmarkDotNet.Analyzers.BenchmarkRunner +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + using System.Collections.Immutable; + using System.Linq; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class RunAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor TypeArgumentClassMissingBenchmarkMethodsRule = new DiagnosticDescriptor(DiagnosticIds.BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Description))); + + internal static readonly DiagnosticDescriptor TypeArgumentClassMustBePublicRule = new DiagnosticDescriptor(DiagnosticIds.BenchmarkRunner_Run_TypeArgumentClassMustBePublic, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMustBePublic_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMustBePublic_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor TypeArgumentClassMustBeUnsealedRule = new DiagnosticDescriptor(DiagnosticIds.BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Description))); + + internal static readonly DiagnosticDescriptor TypeArgumentClassMustBeNonAbstractRule = new DiagnosticDescriptor(DiagnosticIds.BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Description))); + + internal static readonly DiagnosticDescriptor GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttributeRule = new DiagnosticDescriptor(DiagnosticIds.BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Description))); + + + public override ImmutableArray SupportedDiagnostics => + [ + TypeArgumentClassMissingBenchmarkMethodsRule, + TypeArgumentClassMustBePublicRule, + TypeArgumentClassMustBeUnsealedRule, + TypeArgumentClassMustBeNonAbstractRule, + GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttributeRule, + ]; + + public override void Initialize(AnalysisContext analysisContext) + { + analysisContext.EnableConcurrentExecution(); + analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + analysisContext.RegisterCompilationStartAction(ctx => + { + // Only run if BenchmarkDotNet is referenced + var benchmarkRunnerTypeSymbol = ctx.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Running.BenchmarkRunner"); + if (benchmarkRunnerTypeSymbol == null) + { + return; + } + + ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); + }); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + if (context.Node is not InvocationExpressionSyntax invocationExpression) + { + return; + } + + if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpression) + { + return; + } + + if (memberAccessExpression.Expression is not IdentifierNameSyntax identifierNameSyntax) + { + return; + } + + var benchmarkRunnerTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Running.BenchmarkRunner"); + if ( benchmarkRunnerTypeSymbol == null + || benchmarkRunnerTypeSymbol.TypeKind == TypeKind.Error) + { + return; + } + + var classMemberAccessTypeSymbol = context.SemanticModel.GetTypeInfo(identifierNameSyntax).Type; + if ( classMemberAccessTypeSymbol is null + || classMemberAccessTypeSymbol.TypeKind == TypeKind.Error + || !classMemberAccessTypeSymbol.Equals(benchmarkRunnerTypeSymbol, SymbolEqualityComparer.Default)) + { + return; + } + + if (memberAccessExpression.Name.Identifier.ValueText != "Run") + { + return; + } + + INamedTypeSymbol? benchmarkClassTypeSymbol; + Location? diagnosticLocation; + + if (memberAccessExpression.Name is GenericNameSyntax genericMethod) + { + if (genericMethod.TypeArgumentList.Arguments.Count != 1) + { + return; + } + + diagnosticLocation = Location.Create(context.FilterTree, genericMethod.TypeArgumentList.Arguments.Span); + benchmarkClassTypeSymbol = context.SemanticModel.GetTypeInfo(genericMethod.TypeArgumentList.Arguments[0]).Type as INamedTypeSymbol; + } + else + { + if (invocationExpression.ArgumentList.Arguments.Count == 0) + { + return; + } + + // TODO: Support analyzing a collection of typeof() expressions + if (invocationExpression.ArgumentList.Arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpression) + { + return; + } + + diagnosticLocation = typeOfExpression.Type.GetLocation(); + benchmarkClassTypeSymbol = context.SemanticModel.GetTypeInfo(typeOfExpression.Type).Type as INamedTypeSymbol; + + } + + if (benchmarkClassTypeSymbol == null || benchmarkClassTypeSymbol.TypeKind == TypeKind.Error || (benchmarkClassTypeSymbol.IsGenericType && !benchmarkClassTypeSymbol.IsUnboundGenericType)) + { + return; + } + + var benchmarkAttributeTypeSymbol = AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(context.Compilation); + if (benchmarkAttributeTypeSymbol == null) + { + return; + } + + if (!HasBenchmarkAttribute()) + { + ReportDiagnostic(TypeArgumentClassMissingBenchmarkMethodsRule); + } + + if (benchmarkClassTypeSymbol.DeclaredAccessibility != Accessibility.Public) + { + ReportDiagnostic(TypeArgumentClassMustBePublicRule); + } + + if (benchmarkClassTypeSymbol.IsAbstract) + { + ReportDiagnostic(TypeArgumentClassMustBeNonAbstractRule); + } + + if (benchmarkClassTypeSymbol.IsSealed) + { + ReportDiagnostic(TypeArgumentClassMustBeUnsealedRule); + } + + if (benchmarkClassTypeSymbol.IsUnboundGenericType && !AnalyzerHelper.AttributeListContainsAttribute("BenchmarkDotNet.Attributes.GenericTypeArgumentsAttribute", context.Compilation, benchmarkClassTypeSymbol.GetAttributes())) + { + ReportDiagnostic(GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttributeRule); + } + + return; + + bool HasBenchmarkAttribute() + { + var baseType = benchmarkClassTypeSymbol; + + while (baseType != null && baseType.SpecialType != SpecialType.System_Object) + { + foreach (var member in baseType.GetMembers()) + { + if (member is IMethodSymbol { MethodKind: MethodKind.Ordinary }) + { + foreach (var attributeData in member.GetAttributes()) + { + if (attributeData.AttributeClass != null) + { + if (attributeData.AttributeClass.Equals(benchmarkAttributeTypeSymbol, SymbolEqualityComparer.Default)) + { + return true; + } + } + } + } + } + + baseType = baseType.OriginalDefinition.BaseType; + } + + return false; + } + + void ReportDiagnostic(DiagnosticDescriptor diagnosticDescriptor) + { + context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, diagnosticLocation, AnalyzerHelper.NormalizeTypeName(benchmarkClassTypeSymbol))); + } + } + } +} diff --git a/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs b/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs new file mode 100644 index 0000000000..3afdee3e17 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs @@ -0,0 +1,35 @@ +namespace BenchmarkDotNet.Analyzers +{ + public static class DiagnosticIds + { + public const string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods = "BDN1000"; + public const string BenchmarkRunner_Run_TypeArgumentClassMustBePublic = "BDN1001"; + public const string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed = "BDN1002"; + public const string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract = "BDN1003"; + public const string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute = "BDN1004"; + public const string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract = "BDN1100"; + public const string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric = "BDN1101"; + public const string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount = "BDN1102"; + public const string General_BenchmarkClass_MethodMustBePublic = "BDN1103"; + public const string General_BenchmarkClass_MethodMustBeNonGeneric = "BDN1104"; + public const string General_BenchmarkClass_ClassMustBeNonStatic = "BDN1105"; + public const string General_BenchmarkClass_OnlyOneMethodCanBeBaseline = "BDN1106"; + public const string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField = "BDN1200"; + public const string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty = "BDN1201"; + public const string Attributes_GeneralParameterAttributes_FieldMustBePublic = "BDN1202"; + public const string Attributes_GeneralParameterAttributes_PropertyMustBePublic = "BDN1203"; + public const string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField = "BDN1204"; + public const string Attributes_GeneralParameterAttributes_NotValidOnConstantField = "BDN1205"; + public const string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly = "BDN1206"; + public const string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter = "BDN1207"; + public const string Attributes_ParamsAttribute_MustHaveValues = "BDN1300"; + public const string Attributes_ParamsAttribute_MustHaveMatchingValueType = "BDN1301"; + public const string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute = "BDN1302"; + public const string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType = "BDN1303"; + public const string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool = "BDN1304"; + public const string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute = "BDN1400"; + public const string Attributes_ArgumentsAttribute_MethodWithoutAttributeMustHaveNoParameters = "BDN1401"; + public const string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount = "BDN1402"; + public const string Attributes_ArgumentsAttribute_MustHaveMatchingValueType = "BDN1403"; + } +} diff --git a/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs new file mode 100644 index 0000000000..37cd95c9b5 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs @@ -0,0 +1,245 @@ +namespace BenchmarkDotNet.Analyzers.General +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + + using System.Collections.Immutable; + using System.Linq; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class BenchmarkClassAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor ClassWithGenericTypeArgumentsAttributeMustBeNonAbstractRule = new DiagnosticDescriptor(DiagnosticIds.General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract_Description))); + + internal static readonly DiagnosticDescriptor ClassWithGenericTypeArgumentsAttributeMustBeGenericRule = new DiagnosticDescriptor(DiagnosticIds.General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Description))); + + internal static readonly DiagnosticDescriptor GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCountRule = new DiagnosticDescriptor(DiagnosticIds.General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_Description))); + + internal static readonly DiagnosticDescriptor MethodMustBePublicRule = new DiagnosticDescriptor(DiagnosticIds.General_BenchmarkClass_MethodMustBePublic, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_MethodMustBePublic_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_MethodMustBePublic_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_MethodMustBePublic_Description))); + + internal static readonly DiagnosticDescriptor MethodMustBeNonGenericRule = new DiagnosticDescriptor(DiagnosticIds.General_BenchmarkClass_MethodMustBeNonGeneric, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_MethodMustBeNonGeneric_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_MethodMustBeNonGeneric_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_MethodMustBeNonGeneric_Description))); + + internal static readonly DiagnosticDescriptor ClassMustBeNonStaticRule = new DiagnosticDescriptor(DiagnosticIds.General_BenchmarkClass_ClassMustBeNonStatic, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassMustBeNonStatic_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassMustBeNonStatic_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ClassMustBeNonStatic_Description))); + + + internal static readonly DiagnosticDescriptor OnlyOneMethodCanBeBaselineRule = new DiagnosticDescriptor(DiagnosticIds.General_BenchmarkClass_OnlyOneMethodCanBeBaseline, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_OnlyOneMethodCanBeBaseline_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_OnlyOneMethodCanBeBaseline_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => + [ + ClassWithGenericTypeArgumentsAttributeMustBeNonAbstractRule, + ClassWithGenericTypeArgumentsAttributeMustBeGenericRule, + GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCountRule, + MethodMustBePublicRule, + MethodMustBeNonGenericRule, + ClassMustBeNonStaticRule, + OnlyOneMethodCanBeBaselineRule + ]; + + public override void Initialize(AnalysisContext analysisContext) + { + analysisContext.EnableConcurrentExecution(); + analysisContext.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + + analysisContext.RegisterCompilationStartAction(ctx => + { + // Only run if BenchmarkDotNet.Annotations is referenced + var benchmarkAttributeTypeSymbol = AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(ctx.Compilation); + if (benchmarkAttributeTypeSymbol == null) + { + return; + } + + ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ClassDeclaration); + }); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax) + { + return; + } + + var genericTypeArgumentsAttributes = AnalyzerHelper.GetAttributes("BenchmarkDotNet.Attributes.GenericTypeArgumentsAttribute", context.Compilation, classDeclarationSyntax.AttributeLists, context.SemanticModel); + if (genericTypeArgumentsAttributes.Length > 0 && classDeclarationSyntax.TypeParameterList == null) + { + context.ReportDiagnostic(Diagnostic.Create(ClassWithGenericTypeArgumentsAttributeMustBeGenericRule, classDeclarationSyntax.Identifier.GetLocation(), classDeclarationSyntax.Identifier.ToString())); + } + + var benchmarkAttributeTypeSymbol = AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(context.Compilation); + if (benchmarkAttributeTypeSymbol == null) + { + return; + } + + var benchmarkMethodsBuilder = ImmutableArray.CreateBuilder<(MethodDeclarationSyntax Method, ImmutableArray BaselineLocations)>(); + + foreach (var memberDeclarationSyntax in classDeclarationSyntax.Members) + { + if (memberDeclarationSyntax is MethodDeclarationSyntax methodDeclarationSyntax) + { + var benchmarkAttributes = AnalyzerHelper.GetAttributes(benchmarkAttributeTypeSymbol, methodDeclarationSyntax.AttributeLists, context.SemanticModel); + if (benchmarkAttributes.Length > 0) + { + benchmarkMethodsBuilder.Add((methodDeclarationSyntax, benchmarkAttributes.SelectMany(a => GetBaselineLocations(a)).ToImmutableArray())); + } + } + } + + var benchmarkMethods = benchmarkMethodsBuilder.ToImmutable(); + + var classStaticModifier = null as SyntaxToken?; + var classAbstractModifier = null as SyntaxToken?; + + foreach (var modifier in classDeclarationSyntax.Modifiers) + { + if (modifier.IsKind(SyntaxKind.StaticKeyword)) + { + classStaticModifier = modifier; + } + else if (modifier.IsKind(SyntaxKind.AbstractKeyword)) + { + classAbstractModifier = modifier; + } + } + + if (genericTypeArgumentsAttributes.Length == 0) + { + //if (classDeclarationSyntax.TypeParameterList != null && !classAbstractModifier.HasValue) + //{ + // context.ReportDiagnostic(Diagnostic.Create(GenericClassMustBeAbstractOrAnnotatedWithAGenericTypeArgumentsAttributeRule, classDeclarationSyntax.TypeParameterList.GetLocation(), classDeclarationSyntax.Identifier.ToString())); + //} + } + else + { + if (classDeclarationSyntax.TypeParameterList is { Parameters.Count: > 0 }) + { + foreach (var genericTypeArgumentsAttribute in genericTypeArgumentsAttributes) + { + if (genericTypeArgumentsAttribute.ArgumentList is { Arguments.Count: > 0 }) + { + if (genericTypeArgumentsAttribute.ArgumentList.Arguments.Count != classDeclarationSyntax.TypeParameterList.Parameters.Count) + { + context.ReportDiagnostic(Diagnostic.Create(GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCountRule, Location.Create(context.FilterTree, genericTypeArgumentsAttribute.ArgumentList.Arguments.Span), + classDeclarationSyntax.TypeParameterList.Parameters.Count, + classDeclarationSyntax.TypeParameterList.Parameters.Count == 1 ? "" : "s", + classDeclarationSyntax.Identifier.ToString(), + genericTypeArgumentsAttribute.ArgumentList.Arguments.Count)); + } + } + } + + } + } + + if (classAbstractModifier.HasValue && genericTypeArgumentsAttributes.Length > 0) + { + context.ReportDiagnostic(Diagnostic.Create(ClassWithGenericTypeArgumentsAttributeMustBeNonAbstractRule, classAbstractModifier.Value.GetLocation(), classDeclarationSyntax.Identifier.ToString())); + } + + if (benchmarkMethods.Length == 0) + { + return; + } + + if (classStaticModifier.HasValue) + { + context.ReportDiagnostic(Diagnostic.Create(ClassMustBeNonStaticRule, classStaticModifier.Value.GetLocation(), classDeclarationSyntax.Identifier.ToString())); + } + + var baselineCount = 0; + foreach (var (benchmarkMethod, baselineLocations) in benchmarkMethods) + { + var methodIsPublic = benchmarkMethod.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)); + if (!methodIsPublic) + { + context.ReportDiagnostic(Diagnostic.Create(MethodMustBePublicRule, benchmarkMethod.Identifier.GetLocation(), benchmarkMethod.Identifier.ToString())); + } + + if (benchmarkMethod.TypeParameterList != null) + { + context.ReportDiagnostic(Diagnostic.Create(MethodMustBeNonGenericRule, benchmarkMethod.TypeParameterList.GetLocation(), benchmarkMethod.Identifier.ToString())); + } + + baselineCount += baselineLocations.Length; + } + + if (baselineCount > 1) + { + foreach (var benchmarkMethod in benchmarkMethods) + { + foreach (var baselineLocation in benchmarkMethod.BaselineLocations) + { + context.ReportDiagnostic(Diagnostic.Create(OnlyOneMethodCanBeBaselineRule, baselineLocation)); + } + } + } + + return; + + static ImmutableArray GetBaselineLocations(AttributeSyntax attributeSyntax) + { + var baselineLocationsBuilder = ImmutableArray.CreateBuilder(); + + if (attributeSyntax.ArgumentList == null || attributeSyntax.ArgumentList.Arguments.Count == 0) + { + return ImmutableArray.Empty; + } + + foreach (var attributeArgumentSyntax in attributeSyntax.ArgumentList.Arguments) + { + if (attributeArgumentSyntax.NameEquals != null && attributeArgumentSyntax.NameEquals.Name.Identifier.ValueText == "Baseline" && attributeArgumentSyntax.Expression.IsKind(SyntaxKind.TrueLiteralExpression)) + { + baselineLocationsBuilder.Add(attributeArgumentSyntax.GetLocation()); + } + } + + return baselineLocationsBuilder.ToImmutable(); + } + } + } +} diff --git a/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj b/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj index ccc51bcd9a..c1d3c66209 100644 --- a/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj +++ b/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj @@ -14,4 +14,7 @@ + + + \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs new file mode 100644 index 0000000000..ab88d0ec73 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs @@ -0,0 +1,1198 @@ +namespace BenchmarkDotNet.Analyzers.Tests.AnalyzerTests.Attributes +{ + using Fixtures; + + using Analyzers.Attributes; + + using Xunit; + + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Threading.Tasks; + + public class ArgumentsAttributeAnalyzerTests + { + public class General : AnalyzerTestFixture + { + [Theory, CombinatorialData] + public async Task A_method_annotated_with_an_arguments_attribute_with_no_values_and_the_benchmark_attribute_and_having_no_parameters_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(EmptyArgumentsAttributeUsages))] string emptyArgumentsAttributeUsage, + [CombinatorialRange(1, 2)] int attributeUsageMultiplier) + { + var emptyArgumentsAttributeUsages = new List(); + + for (var i = 0; i < attributeUsageMultiplier; i++) + { + emptyArgumentsAttributeUsages.Add(emptyArgumentsAttributeUsage); + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + {{string.Join("\n", emptyArgumentsAttributeUsages)}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + public static IEnumerable EmptyArgumentsAttributeUsages() + { + yield return "[Arguments]"; + yield return "[Arguments()]"; + yield return "[Arguments(Priority = 1)]"; + + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "[Arguments({0}new object[] {{ }}{1})]", + "[Arguments({0}new object[0]{1})]", + "[Arguments({0}[]{1})]", + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + } + } + } + } + } + + public class RequiresBenchmarkAttribute : AnalyzerTestFixture + { + public RequiresBenchmarkAttribute() : base(ArgumentsAttributeAnalyzer.RequiresBenchmarkAttributeRule) { } + + [Theory] + [MemberData(nameof(ArgumentAttributeUsagesListLength))] + public async Task A_method_annotated_with_at_least_one_arguments_attribute_together_with_the_benchmark_attribute_should_not_trigger_diagnostic(int argumentAttributeUsagesListLength) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{string.Join("]\n[", ArgumentAttributeUsages.Take(argumentAttributeUsagesListLength))}}] + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(ArgumentAttributeUsagesListLength))] + public async Task A_method_with_at_least_one_arguments_attribute_but_no_benchmark_attribute_should_trigger_diagnostic(int argumentAttributeUsagesListLength) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join("\n", ArgumentAttributeUsages.Take(argumentAttributeUsagesListLength).Select((a, i) => $"[{{|#{i}:{a}|}}]"))}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + for (var i = 0; i < argumentAttributeUsagesListLength; i++) + { + AddExpectedDiagnostic(i); + } + + await RunAsync(); + } + + public static TheoryData ArgumentAttributeUsagesListLength => new(Enumerable.Range(1, ArgumentAttributeUsages.Count)); + + private static ReadOnlyCollection ArgumentAttributeUsages => new List { + "Arguments", + "Arguments()", + "Arguments(42, \"test\")" + }.AsReadOnly(); + } + + public class MethodWithoutAttributeMustHaveNoParameters : AnalyzerTestFixture + { + public MethodWithoutAttributeMustHaveNoParameters() : base(ArgumentsAttributeAnalyzer.MethodWithoutAttributeMustHaveNoParametersRule) { } + + [Fact] + public async Task A_method_annotated_with_an_arguments_attribute_and_the_benchmark_attribute_and_having_parameters_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [Arguments(42, "test")] + public void BenchmarkMethod(int a, string b) + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(ParametersListLength))] + public async Task A_method_with_parameters_and_no_arguments_or_benchmark_attributes_should_not_trigger_diagnostic(int parametersListLength) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public void BenchmarkMethod({{string.Join(", ", Parameters.Take(parametersListLength))}}) + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(ParametersListLength))] + public async Task A_method_annotated_with_the_benchmark_attribute_but_no_arguments_attribute_with_parameters_should_trigger_diagnostic(int parametersListLength) + { + const string benchmarkMethodName = "BenchmarkMethod"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + public void {{benchmarkMethodName}}({|#0:{{string.Join(", ", Parameters.Take(parametersListLength))}}|}) + { + + } + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(benchmarkMethodName); + + await RunAsync(); + } + + public static TheoryData ParametersListLength => new(Enumerable.Range(1, Parameters.Count)); + + private static ReadOnlyCollection Parameters => new List { + "int a", + "string b", + "bool c" + }.AsReadOnly(); + } + + public class MustHaveMatchingValueCount : AnalyzerTestFixture + { + public MustHaveMatchingValueCount() : base(ArgumentsAttributeAnalyzer.MustHaveMatchingValueCountRule) { } + + [Fact] + public async Task A_method_not_annotated_with_any_arguments_attributes_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(ArgumentsAttributeUsages))] + public async Task Having_a_matching_value_count_should_not_trigger_diagnostic(string argumentsAttributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + {{argumentsAttributeUsage}} + public void BenchmarkMethod(string a, bool b) + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(EmptyArgumentsAttributeUsagesWithLocationMarker))] + public async Task Having_a_mismatching_empty_value_count_targeting_a_method_with_parameters_should_trigger_diagnostic(string argumentsAttributeUsage) + { + const string benchmarkMethodName = "BenchmarkMethod"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + {{argumentsAttributeUsage}} + public void {{benchmarkMethodName}}(string a) + { + + } + } + """; + TestCode = testCode; + AddDefaultExpectedDiagnostic(1, "", benchmarkMethodName, 0); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Having_a_mismatching_value_count_should_trigger_diagnostic([CombinatorialMemberData(nameof(ArgumentsAttributeUsagesWithLocationMarker))] string argumentsAttributeUsage, + [CombinatorialMemberData(nameof(ParameterLists))] (string Parameters, int ParameterCount, string PluralSuffix) parameterData) + { + const string benchmarkMethodName = "BenchmarkMethod"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + {{argumentsAttributeUsage}} + public void {{benchmarkMethodName}}({{parameterData.Parameters}}) + { + + } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, parameterData.ParameterCount, parameterData.PluralSuffix, benchmarkMethodName, 2); + AddExpectedDiagnostic(1, parameterData.ParameterCount, parameterData.PluralSuffix, benchmarkMethodName, 3); + + await RunAsync(); + } + + public static IEnumerable<(string, int, string)> ParameterLists => [ ("string a", 1, ""), ("", 0, "s") ]; + + public static TheoryData ArgumentsAttributeUsages() + { + return new TheoryData(GenerateData()); + + static IEnumerable GenerateData() + { + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "[Arguments({1}{2})]", + "[Arguments({0}new object[] {{ {1} }}{2})]", + "[Arguments({0}[ {1} ]{2})]" + }; + + var valueLists = new List + { + "42, \"test\"", + "\"value\", 100" + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Join("\n ", valueLists.Select(vv => string.Format(attributeUsageBase, nameColonUsage, vv, priorityNamedParameterUsage))); + } + } + } + } + } + + public static TheoryData EmptyArgumentsAttributeUsagesWithLocationMarker() + { + return new TheoryData(GenerateData()); + + static IEnumerable GenerateData() + { + yield return "[{|#0:Arguments|}]"; + yield return "[Arguments{|#0:()|}]"; + yield return "[Arguments({|#0:Priority = 1|})]"; + + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "[Arguments({0}new object[] {{|#0:{{ }}|}}{1})]", + "[Arguments({0}new object[{{|#0:0|}}]{1})]", + "[Arguments({0}{{|#0:[]|}}{1})]", + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + } + } + } + } + } + + public static IEnumerable ArgumentsAttributeUsagesWithLocationMarker() + { + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "[Arguments({{|#{1}:{2}|}}{3})]", + "[Arguments({0}new object[] {{ {{|#{1}:{2}|}} }}{3})]", + "[Arguments({0}[ {{|#{1}:{2}|}} ]{3})]" + }; + + var valueLists = new List + { + "42, \"test\"", + "\"value\", 100, false" + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Join("\n ", valueLists.Select((vv, i) => string.Format(attributeUsageBase, nameColonUsage, i, vv, priorityNamedParameterUsage))); + } + } + } + } + } + + public class MustHaveMatchingValueType : AnalyzerTestFixture + { + public MustHaveMatchingValueType() : base(ArgumentsAttributeAnalyzer.MustHaveMatchingValueTypeRule) { } + + [Fact] + public async Task A_method_not_annotated_with_any_arguments_attributes_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(EmptyArgumentsAttributeUsagesWithMismatchingValueCount))] + public async Task Having_a_mismatching_value_count_with_empty_argument_attribute_usages_should_not_trigger_diagnostic(string argumentsAttributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + public class BenchmarkClass + { + [Benchmark] + {{argumentsAttributeUsage}} + public void BenchmarkMethod(string a) + { + + } + } + """; + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Having_a_mismatching_value_count_with_nonempty_argument_attribute_usages_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument, + [CombinatorialValues("string a", "")] string parameters) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + public class BenchmarkClass + { + [Benchmark] + [{{string.Format(scalarValuesContainerAttributeArgument, """ + 42, "test" + """)}}] + [{{string.Format(scalarValuesContainerAttributeArgument, """ + "value", 100, true + """)}}] + public void BenchmarkMethod({{parameters}}) + { + + } + } + """; + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_expected_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, valueAndType.Value1)}}] + public void BenchmarkMethod({{valueAndType.Value2}} a) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_integer_value_types_within_target_type_range_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(IntegerValuesAndTypesWithinTargetTypeRange))] ValueTupleDouble integerValueAndType, + bool explicitCast) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, $"{(explicitCast ? $"({integerValueAndType.Value2})" : "")}{integerValueAndType.Value1}")}}] + public void BenchmarkMethod({{integerValueAndType.Value2}} a) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_null_to_nullable_struct_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NullableStructTypes))] string type, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, "null")}}] + public void BenchmarkMethod({{type}}? a) + { + + } + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_expected_constant_value_type_should_not_trigger_diagnostic(bool useConstantFromOtherClass, + bool useLocalConstant, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ConstantValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{(useLocalConstant ? $"private const {valueAndType.Value2} _x = {(useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1!)};" : "")}} + + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, useLocalConstant ? "_x" : useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1!)}}] + public void BenchmarkMethod({{valueAndType.Value2}} a) + { + + } + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + if (useConstantFromOtherClass) + { + ReferenceConstants($"{valueAndType.Value2!}", valueAndType.Value1!); + } + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_expected_null_reference_constant_value_type_should_not_trigger_diagnostic(bool useConstantFromOtherClass, + bool useLocalConstant, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NullReferenceConstantTypes))] string type, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{(useLocalConstant ? $"private const {type}? _x = {(useConstantFromOtherClass ? "Constants.Value" : "null")};" : "")}} + + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, useLocalConstant ? "_x" : useConstantFromOtherClass ? "Constants.Value" : "null")}}] + public void BenchmarkMethod({{type}} a) + { + + } + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + if (useConstantFromOtherClass) + { + ReferenceConstants($"{type}?", "null"); + } + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_implicitly_convertible_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument, + [CombinatorialValues("(byte)42", "'c'")] string value) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, value)}}] + public void BenchmarkMethod(int a) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_implicitly_convertible_array_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, "new[] { 0, 1, 2 }")}}] + public void BenchmarkMethod(System.Span a) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Having_unknown_parameter_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, "42, \"test\"")}}] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, "43, \"test2\"")}}] + public void BenchmarkMethod(unkown a, string b) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_unkown_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, "{|#0:dummy_literal|}, true")}}] + public void BenchmarkMethod(byte a, bool b) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_unexpected_or_not_implicitly_convertible_value_type_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NotConvertibleValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + const string expectedArgumentType = "decimal"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, $"{{|#0:{valueAndType.Value1}|}}")}}] + public void BenchmarkMethod({{expectedArgumentType}} a) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + AddDefaultExpectedDiagnostic(valueAndType.Value1!, expectedArgumentType, valueAndType.Value2!); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_unexpected_or_not_implicitly_convertible_constant_value_type_should_trigger_diagnostic(bool useConstantFromOtherClass, + bool useLocalConstant, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NotConvertibleConstantValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + const string expectedArgumentType = "decimal"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{(useLocalConstant ? $"private const {valueAndType.Value2} _x = {(useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1)};" : "")}} + + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, $"{{|#0:{(useLocalConstant ? "_x" : useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1)}|}}")}}] + public void BenchmarkMethod({{expectedArgumentType}} a) + { + + } + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + if (useConstantFromOtherClass) + { + ReferenceConstants(valueAndType.Value2!, valueAndType.Value1!); + } + + AddDefaultExpectedDiagnostic(useLocalConstant ? "_x" : useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1!, expectedArgumentType, valueAndType.Value2!); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_null_to_nonnullable_struct_value_type_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NullableStructTypes))] string type, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, "{|#0:null|}")}}] + public void BenchmarkMethod({{type}} a) + { + + } + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + AddDefaultExpectedDiagnostic("null", type, "null"); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_a_not_implicitly_convertible_array_value_type_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument, + [CombinatorialValues("System.Span", "string[]")] string expectedArgumentType) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, "{|#0:new[] { 0, 1, 2 }|}")}}] + public void BenchmarkMethod({{expectedArgumentType}} a) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + AddDefaultExpectedDiagnostic("new[] { 0, 1, 2 }", expectedArgumentType, "int[]"); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_integer_value_types_not_within_target_type_range_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerableLocal))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(IntegerValuesAndTypesNotWithinTargetTypeRange))] ValueTupleTriple integerValueAndType) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + [{{dummyAttributeUsage}}{{string.Format(scalarValuesContainerAttributeArgument, $"{{|#0:{integerValueAndType.Value1}|}}")}}] + public void BenchmarkMethod({{integerValueAndType.Value2}} a) + { + + } + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + AddDefaultExpectedDiagnostic(integerValueAndType.Value1!, integerValueAndType.Value2!, integerValueAndType.Value3!); + + await RunAsync(); + } + + public static IEnumerable DummyAttributeUsage => DummyAttributeUsageTheoryData; + + public static TheoryData EmptyArgumentsAttributeUsagesWithMismatchingValueCount() + { + return new TheoryData(GenerateData()); + + static IEnumerable GenerateData() + { + yield return "[Arguments]"; + yield return "[Arguments()]"; + yield return "[Arguments(Priority = 1)]"; + + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "[Arguments({0}new object[] {{ }}{1})]", + "[Arguments({0}new object[0]{1})]", + "[Arguments({0}[]{1})]", + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + } + } + } + } + } + + public static IEnumerable ScalarValuesContainerAttributeArgumentEnumerableLocal => ScalarValuesContainerAttributeArgumentEnumerable(); + + public static IEnumerable> IntegerValuesAndTypesWithinTargetTypeRange => + [ + // byte (0 to 255) + ("0", "byte"), + ("100", "byte"), + ("255", "byte"), + + // sbyte (-128 to 127) + ("-128", "sbyte"), + ("0", "sbyte"), + ("127", "sbyte"), + + // short (-32,768 to 32,767) + ("-32768", "short"), + ("0", "short"), + ("32767", "short"), + + // ushort (0 to 65,535) + ("0", "ushort"), + ("1000", "ushort"), + ("65535", "ushort"), + + // int (-2,147,483,648 to 2,147,483,647) + ("-2147483648", "int"), + ("0", "int"), + ("2147483647", "int"), + + // uint (0 to 4,294,967,295) + ("0", "uint"), + ("1000000", "uint"), + ("4294967295", "uint"), + + // long (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807) + ("-9223372036854775808", "long"), + ("0", "long"), + ("9223372036854775807", "long"), + + // ulong (0 to 18,446,744,073,709,551,615) + ("0", "ulong"), + ("1000000", "ulong"), + ("18446744073709551615", "ulong"), + ]; + + public static IEnumerable> IntegerValuesAndTypesNotWithinTargetTypeRange => + [ + // byte (0 to 255) - out of range values + ("-1", "byte", "int"), + ("256", "byte", "int"), + ("1000", "byte", "int"), + + // sbyte (-128 to 127) - out of range values + ("-129", "sbyte", "int"), + ("128", "sbyte", "int"), + ("500", "sbyte", "int"), + + // short (-32,768 to 32,767) - out of range values + ("-32769", "short", "int"), + ("32768", "short", "int"), + ("100000", "short", "int"), + + // ushort (0 to 65,535) - out of range values + ("-1", "ushort", "int"), + ("65536", "ushort", "int"), + ("100000", "ushort", "int"), + + // int (-2,147,483,648 to 2,147,483,647) - out of range values + ("-2147483649", "int", "long"), + ("2147483648", "int", "uint"), + ("5000000000", "int", "long"), + + // uint (0 to 4,294,967,295) - out of range values + ("-1", "uint", "int"), + ("4294967296", "uint", "long"), + ("5000000000", "uint", "long"), + + // long - out of range values (exceeding long range) + ("9223372036854775808", "long", "ulong"), + + // ulong - negative values + ("-1", "ulong", "int"), + ("-100", "ulong", "int"), + ("-9223372036854775808", "ulong", "long"), + ]; + + public static IEnumerable> ValuesAndTypes => + [ + ( "true", "bool" ), + ( "(byte)123", "byte" ), + ( "'A'", "char" ), + ( "1.0D", "double" ), + ( "1.0F", "float" ), + ( "123", "int" ), + ( "123L", "long" ), + ( "(sbyte)-100", "sbyte" ), + ( "(short)-123", "short" ), + ( """ + "test" + """, "string" ), + ( "123U", "uint" ), + ( "123UL", "ulong" ), + ( "(ushort)123", "ushort" ), + + ( """ + (object)"test_object" + """, "object" ), + ( "typeof(string)", "System.Type" ), + ( "DummyEnum.Value1", "DummyEnum" ), + + ( "new[] { 0, 1, 2 }", "int[]" ), + ]; + + public static IEnumerable> NotConvertibleValuesAndTypes => + [ + ( "true", "bool" ), + ( "1.0D", "double" ), + ( "1.0F", "float" ), + ( """ + "test" + """, "string" ), + + ( """ + (object)"test_object" + """, "object" ), + ( "typeof(string)", "System.Type" ), + ( "DummyEnum.Value1", "DummyEnum" ) + ]; + + public static IEnumerable> ConstantValuesAndTypes => + [ + ( "true", "bool" ), + ( "(byte)123", "byte" ), + ( "'A'", "char" ), + ( "1.0D", "double" ), + ( "1.0F", "float" ), + ( "123", "int" ), + ( "123L", "long" ), + ( "(sbyte)-100", "sbyte" ), + ( "(short)-123", "short" ), + ( """ + "test" + """, "string" ), + ( "123U", "uint" ), + ( "123UL", "ulong" ), + ( "(ushort)123", "ushort" ), + + ( "DummyEnum.Value1", "DummyEnum" ), + ]; + + public static IEnumerable NullableStructTypes => + [ + "bool", + "byte", + "char", + "double", + "float", + "int", + "long", + "sbyte", + "short", + "uint", + "ulong", + "ushort", + "DummyEnum", + ]; + + public static IEnumerable NullReferenceConstantTypes => + [ + "object", + "string", + "System.Type", + ]; + + public static IEnumerable> NotConvertibleConstantValuesAndTypes => + [ + ( "true", "bool" ), + ( "1.0D", "double" ), + ( "1.0F", "float" ), + ( """ + "test" + """, "string" ), + + ( "DummyEnum.Value1", "DummyEnum" ) + ]; + } + + public static TheoryData DummyAttributeUsageTheoryData => [ + "", + "Dummy, " + ]; + + private static IEnumerable ScalarValuesContainerAttributeArgumentEnumerable() + { + return GenerateData().Distinct(); + + static IEnumerable GenerateData() + { + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "Arguments({{0}}{1})", + "Arguments({0}new object[] {{{{ {{0}} }}}}{1})", + "Arguments({0}[ {{0}} ]{1})" + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + } + } + } + } + + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/GeneralParameterAttributesAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/GeneralParameterAttributesAnalyzerTests.cs new file mode 100644 index 0000000000..ea30028c12 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/GeneralParameterAttributesAnalyzerTests.cs @@ -0,0 +1,1319 @@ +namespace BenchmarkDotNet.Analyzers.Tests.AnalyzerTests.Attributes +{ + using Fixtures; + + using Analyzers.Attributes; + + using Xunit; + + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Threading.Tasks; + + public class GeneralParameterAttributesAnalyzerTests + { + public class MutuallyExclusiveOnField : AnalyzerTestFixture + { + public MutuallyExclusiveOnField() : base(GeneralParameterAttributesAnalyzer.MutuallyExclusiveOnFieldRule) { } + + [Fact] + public async Task A_field_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + public int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task A_field_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + [Dummy] + public int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Fact] + public async Task A_field_annotated_with_a_duplicate_nonparameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + [Dummy] + [Dummy] + public int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributeUsages))] + public async Task A_field_annotated_with_a_unique_parameter_attribute_should_not_trigger_diagnostic(string attributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateSameParameterAttributeUsages))] + public async Task A_field_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic(string currentUniqueAttributeUsage, int currentUniqueAttributeUsagePosition, int[] counts) + { + var duplicateAttributeUsages = new List(1 + counts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < counts.Length; i++) + { + if (i == currentUniqueAttributeUsagePosition) + { + duplicateAttributeUsages.Add($"[{currentUniqueAttributeUsage}]"); + } + + for (var j = 0; j < counts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateParameterAttributeUsageCounts))] + public async Task A_field_annotated_with_more_than_one_parameter_attribute_should_trigger_diagnostic_for_each_attribute_usage(int[] duplicateAttributeUsageCounts) + { + const string fieldIdentifier = "_field"; + + var duplicateAttributeUsages = new List(duplicateAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + var diagnosticCounter = 0; + for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) + { + for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{{|#{diagnosticCounter++}:{uniqueParameterAttributeUsages[i]}|}}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public int {{fieldIdentifier}} = 0, field2 = 1; + } + """; + + TestCode = testCode; + + for (var i = 0; i < diagnosticCounter; i++) + { + AddExpectedDiagnostic(i, fieldIdentifier); + } + + await RunAsync(); + } + + public static TheoryData UniqueParameterAttributeUsages => new(UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)); + + public static TheoryData DuplicateSameParameterAttributeUsages => DuplicateSameAttributeUsagesTheoryData; + + public static TheoryData DuplicateParameterAttributeUsageCounts => DuplicateAttributeUsageCountsTheoryData; + } + + public class MutuallyExclusiveOnProperty : AnalyzerTestFixture + { + public MutuallyExclusiveOnProperty() : base(GeneralParameterAttributesAnalyzer.MutuallyExclusiveOnPropertyRule) { } + + [Fact] + public async Task A_property_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + public int Property { get; set; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task A_property_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + [Dummy] + public int Property { get; set; } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Fact] + public async Task A_property_annotated_with_a_duplicate_nonparameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + [Dummy] + [Dummy] + public int Property { get; set; } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributeUsages))] + public async Task A_property_annotated_with_a_unique_parameter_attribute_should_not_trigger_diagnostic(string attributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public int Property { get; set; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateSameParameterAttributeUsages))] + public async Task A_property_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic(string currentAttributeUsage, int currentUniqueAttributeUsagePosition, int[] duplicateSameAttributeUsageCounts) + { + var duplicateAttributeUsages = new List(1 + duplicateSameAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateSameAttributeUsageCounts.Length; i++) + { + if (i == currentUniqueAttributeUsagePosition) + { + duplicateAttributeUsages.Add($"[{currentAttributeUsage}]"); + } + + for (var j = 0; j < duplicateSameAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public int Property { get; set; } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateParameterAttributeUsages))] + public async Task A_property_annotated_with_more_than_one_parameter_attribute_should_trigger_diagnostic_for_each_attribute_usage(int[] duplicateAttributeUsageCounts) + { + const string propertyIdentifier = "Property"; + + var duplicateAttributeUsages = new List(duplicateAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + var diagnosticCounter = 0; + for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) + { + for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{{|#{diagnosticCounter++}:{uniqueParameterAttributeUsages[i]}|}}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public int {{propertyIdentifier}} { get; set; } + } + """; + + TestCode = testCode; + + for (var i = 0; i < diagnosticCounter; i++) + { + AddExpectedDiagnostic(i, propertyIdentifier); + } + + await RunAsync(); + } + + public static TheoryData UniqueParameterAttributeUsages => new(UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)); + + public static TheoryData DuplicateSameParameterAttributeUsages => DuplicateSameAttributeUsagesTheoryData; + + public static TheoryData DuplicateParameterAttributeUsages => DuplicateAttributeUsageCountsTheoryData; + } + + public class FieldMustBePublic : AnalyzerTestFixture + { + public FieldMustBePublic() : base(GeneralParameterAttributesAnalyzer.FieldMustBePublic) { } + + [Theory] + [ClassData(typeof(NonPublicClassMemberAccessModifiersTheoryData))] + public async Task A_nonpublic_field_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic(string classMemberAccessModifier) + { + var testCode = /* lang=c#-test */ $$""" + public class BenchmarkClass + { + {{classMemberAccessModifier}}int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [ClassData(typeof(NonPublicClassMemberAccessModifiersTheoryData))] + public async Task A_nonpublic_field_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic(string classMemberAccessModifier) + { + var testCode = /* lang=c#-test */ $$""" + public class BenchmarkClass + { + [Dummy] + {{classMemberAccessModifier}}int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributeUsages))] + public async Task A_public_field_annotated_with_a_unique_parameter_attribute_should_not_trigger_diagnostic(string attributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public int _field = 0, _field2 = 2; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_nonpublic_field_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DuplicateSameParameterAttributeUsages))] (string CurrentUniqueAttributeUsage, int CurrentUniqueAttributeUsagePosition, int[] Counts) duplicateSameParameterAttributeUsages, + [CombinatorialMemberData(nameof(NonPublicClassMemberAccessModifiers))] string classMemberAccessModifier) + { + var duplicateAttributeUsages = new List(1 + duplicateSameParameterAttributeUsages.Counts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateSameParameterAttributeUsages.Counts.Length; i++) + { + if (i == duplicateSameParameterAttributeUsages.CurrentUniqueAttributeUsagePosition) + { + duplicateAttributeUsages.Add($"[{duplicateSameParameterAttributeUsages.CurrentUniqueAttributeUsage}]"); + } + + for (var j = 0; j < duplicateSameParameterAttributeUsages.Counts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + {{classMemberAccessModifier}}int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateAttributeUsageCountsAndNonPublicClassMemberAccessModifiersCombinations))] + public async Task A_nonpublic_field_annotated_with_more_than_one_parameter_attribute_should_not_trigger_diagnostic(int[] duplicateAttributeUsageCounts, string classMemberAccessModifier) + { + var duplicateAttributeUsages = new List(duplicateAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) + { + for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + {{classMemberAccessModifier}}int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_nonpublic_field_annotated_with_a_unique_parameter_attribute_should_trigger_diagnostic([CombinatorialMemberData(nameof(UniqueParameterAttributes))] (string AttributeName, string AttributeUsage) attribute, + [CombinatorialMemberData(nameof(NonPublicClassMemberAccessModifiers))] string classMemberAccessModifier) + { + const string fieldIdentifier = "_field"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attribute.AttributeUsage}}] + {{classMemberAccessModifier}}int {|#0:{{fieldIdentifier}}|} = 0, field2 = 0; + } + """; + TestCode = testCode; + AddDefaultExpectedDiagnostic(fieldIdentifier, attribute.AttributeName); + + await RunAsync(); + } + + public static IEnumerable DuplicateAttributeUsageCountsAndNonPublicClassMemberAccessModifiersCombinations => CombinationsGenerator.CombineArguments(DuplicateParameterAttributeUsageCounts, NonPublicClassMemberAccessModifiers); + + public static TheoryData UniqueParameterAttributeUsages => new(UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)); + + public static IEnumerable<(string AttributeName, string AttributeUsage)> UniqueParameterAttributes => UniqueParameterAttributesTheoryData.Select(tdr => ((tdr[0] as string)!, (tdr[1] as string)!)); + + public static IEnumerable NonPublicClassMemberAccessModifiers => new NonPublicClassMemberAccessModifiersTheoryData(); + + public static IEnumerable<(string CurrentUniqueAttributeUsage, int CurrentUniqueAttributeUsagePosition, int[] Counts)> DuplicateSameParameterAttributeUsages => DuplicateSameAttributeUsagesTheoryData.Select(tdr => ((tdr[0] as string)!, (int)tdr[1], (tdr[2] as int[])!)); + + public static IEnumerable DuplicateParameterAttributeUsageCounts => DuplicateAttributeUsageCountsTheoryData; + } + + public class PropertyMustBePublic : AnalyzerTestFixture + { + public PropertyMustBePublic() : base(GeneralParameterAttributesAnalyzer.PropertyMustBePublic) { } + + [Theory] + [ClassData(typeof(NonPublicClassMemberAccessModifiersTheoryData))] + public async Task A_nonpublic_property_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic(string classMemberAccessModifier) + { + var testCode = /* lang=c#-test */ $$""" + public class BenchmarkClass + { + {{classMemberAccessModifier}}int Property { get; set; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [ClassData(typeof(NonPublicClassMemberAccessModifiersTheoryData))] + public async Task A_nonpublic_property_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic(string classMemberAccessModifier) + { + var testCode = /* lang=c#-test */ $$""" + public class BenchmarkClass + { + [Dummy] + {{classMemberAccessModifier}}int Property { get; set; } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributeUsages))] + public async Task A_public_property_annotated_with_a_unique_parameter_attribute_should_not_trigger_diagnostic(string attributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public int Property { get; set; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_nonpublic_property_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DuplicateSameParameterAttributeUsages))] (string CurrentUniqueAttributeUsage, int CurrentUniqueAttributeUsagePosition, int[] Counts) duplicateSameParameterAttributeUsages, + [CombinatorialMemberData(nameof(NonPublicClassMemberAccessModifiers))] string classMemberAccessModifier) + { + var duplicateAttributeUsages = new List(1 + duplicateSameParameterAttributeUsages.Counts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateSameParameterAttributeUsages.Counts.Length; i++) + { + if (i == duplicateSameParameterAttributeUsages.CurrentUniqueAttributeUsagePosition) + { + duplicateAttributeUsages.Add($"[{duplicateSameParameterAttributeUsages.CurrentUniqueAttributeUsage}]"); + } + + for (var j = 0; j < duplicateSameParameterAttributeUsages.Counts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + {{classMemberAccessModifier}}int Property { get; set; } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateAttributeUsageCountsAndNonPublicClassMemberAccessModifiersCombinations))] + public async Task A_nonpublic_property_annotated_with_more_than_one_parameter_attribute_should_not_trigger_diagnostic(int[] duplicateAttributeUsageCounts, string classMemberAccessModifier) + { + var duplicateAttributeUsages = new List(duplicateAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) + { + for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + {{classMemberAccessModifier}}int Property { get; set; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_nonpublic_property_annotated_with_a_unique_parameter_attribute_should_trigger_diagnostic([CombinatorialMemberData(nameof(UniqueParameterAttributes))] (string AttributeName, string AttributeUsage) attribute, + [CombinatorialMemberData(nameof(NonPublicClassMemberAccessModifiers))] string classMemberAccessModifier) + { + const string propertyIdentifier = "Property"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attribute.AttributeUsage}}] + {{classMemberAccessModifier}}int {|#0:{{propertyIdentifier}}|} { get; set; } + } + """; + TestCode = testCode; + AddDefaultExpectedDiagnostic(propertyIdentifier, attribute.AttributeName); + + await RunAsync(); + } + + public static IEnumerable DuplicateAttributeUsageCountsAndNonPublicClassMemberAccessModifiersCombinations => CombinationsGenerator.CombineArguments(DuplicateParameterAttributeUsageCounts, NonPublicClassMemberAccessModifiers); + + public static TheoryData UniqueParameterAttributeUsages => new(UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)); + + public static IEnumerable<(string AttributeName, string AttributeUsage)> UniqueParameterAttributes => UniqueParameterAttributesTheoryData.Select(tdr => ((tdr[0] as string)!, (tdr[1] as string)!)); + + public static IEnumerable NonPublicClassMemberAccessModifiers => new NonPublicClassMemberAccessModifiersTheoryData(); + + public static IEnumerable<(string CurrentUniqueAttributeUsage, int CurrentUniqueAttributeUsagePosition, int[] Counts)> DuplicateSameParameterAttributeUsages => DuplicateSameAttributeUsagesTheoryData.Select(tdr => ((tdr[0] as string)!, (int)tdr[1], (tdr[2] as int[])!)); + + public static TheoryData DuplicateParameterAttributeUsageCounts => DuplicateAttributeUsageCountsTheoryData; + } + + public class NotValidOnReadonlyField : AnalyzerTestFixture + { + public NotValidOnReadonlyField() : base(GeneralParameterAttributesAnalyzer.NotValidOnReadonlyFieldRule) { } + + [Fact] + public async Task A_readonly_field_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + public readonly int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task A_readonly_field_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + [Dummy] + public readonly int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributeUsages))] + public async Task A_field_without_a_readonly_modifier_annotated_with_a_unique_parameter_attribute_should_not_trigger_diagnostic(string attributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateSameParameterAttributeUsages))] + public async Task A_readonly_field_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic(string currentAttributeUsage, int currentUniqueAttributeUsagePosition, int[] duplicateSameAttributeUsageCounts) + { + var duplicateAttributeUsages = new List(1 + duplicateSameAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateSameAttributeUsageCounts.Length; i++) + { + if (i == currentUniqueAttributeUsagePosition) + { + duplicateAttributeUsages.Add($"[{currentAttributeUsage}]"); + } + + for (var j = 0; j < duplicateSameAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public readonly int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateParameterAttributeUsageCounts))] + public async Task A_readonly_field_annotated_with_more_than_one_parameter_attribute_should_not_trigger_diagnostic(int[] duplicateAttributeUsageCounts) + { + var duplicateAttributeUsages = new List(duplicateAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) + { + for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public readonly int _field = 0, _field2 = 1; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributes))] + public async Task A_readonly_field_annotated_with_a_unique_parameter_attribute_should_trigger_diagnostic(string attributeName, string attributeUsage) + { + const string fieldIdentifier = "_field"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public {|#0:readonly|} int {{fieldIdentifier}} = 0, field2 = 1; + } + """; + TestCode = testCode; + AddDefaultExpectedDiagnostic(fieldIdentifier, attributeName); + + await RunAsync(); + } + + public static TheoryData UniqueParameterAttributeUsages => new(UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)); + + public static TheoryData UniqueParameterAttributes => UniqueParameterAttributesTheoryData; + + public static TheoryData DuplicateSameParameterAttributeUsages => DuplicateSameAttributeUsagesTheoryData; + + public static TheoryData DuplicateParameterAttributeUsageCounts => DuplicateAttributeUsageCountsTheoryData; + } + + public class NotValidOnConstantField : AnalyzerTestFixture + { + public NotValidOnConstantField() : base(GeneralParameterAttributesAnalyzer.NotValidOnConstantFieldRule) { } + + [Fact] + public async Task A_constant_field_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + public const int Constant = 0; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task A_constant_field_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + [Dummy] + public const int Constant = 0; + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateSameParameterAttributeUsages))] + public async Task A_constant_field_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic(string currentAttributeUsage, int currentUniqueAttributeUsagePosition, int[] duplicateSameAttributeUsageCounts) + { + var duplicateAttributeUsages = new List(1 + duplicateSameAttributeUsageCounts.Sum()); + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateSameAttributeUsageCounts.Length; i++) + { + if (i == currentUniqueAttributeUsagePosition) + { + duplicateAttributeUsages.Add($"[{currentAttributeUsage}]"); + } + + for (var j = 0; j < duplicateSameAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public const int Constant = 0; + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateParameterAttributeUsageCounts))] + public async Task A_constant_field_annotated_with_more_than_one_parameter_attribute_should_not_trigger_diagnostic(int[] duplicateAttributeUsageCounts) + { + var duplicateAttributeUsages = new List(duplicateAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) + { + for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public const int Constant = 0; + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributes))] + public async Task A_constant_field_annotated_with_a_unique_parameter_attribute_should_trigger_diagnostic(string attributeName, string attributeUsage) + { + const string constantIdentifier = "Constant"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public {|#0:const|} int {{constantIdentifier}} = 0; + } + """; + TestCode = testCode; + AddDefaultExpectedDiagnostic(constantIdentifier, attributeName); + + await RunAsync(); + } + + public static TheoryData UniqueParameterAttributeUsages => new(UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)); + + public static TheoryData UniqueParameterAttributes => UniqueParameterAttributesTheoryData; + + public static TheoryData DuplicateSameParameterAttributeUsages => DuplicateSameAttributeUsagesTheoryData; + + public static TheoryData DuplicateParameterAttributeUsageCounts => DuplicateAttributeUsageCountsTheoryData; + } +#if NET5_0_OR_GREATER + public class PropertyCannotBeInitOnly : AnalyzerTestFixture + { + public PropertyCannotBeInitOnly() : base(GeneralParameterAttributesAnalyzer.PropertyCannotBeInitOnlyRule) { } + + [Fact] + public async Task An_initonly_property_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + public int Property { get; init; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task An_initonly_property_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + public class BenchmarkClass + { + [Dummy] + public int Property { get; init; } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributeUsages))] + public async Task A_property_with_an_assignable_setter_annotated_with_a_unique_parameter_attribute_should_not_trigger_diagnostic(string attributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public int Property { get; set; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateSameParameterAttributeUsages))] + public async Task An_initonly_property_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic(string currentAttributeUsage, int currentUniqueAttributeUsagePosition, int[] duplicateSameAttributeUsageCounts) + { + var duplicateAttributeUsages = new List(1 + duplicateSameAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateSameAttributeUsageCounts.Length; i++) + { + if (i == currentUniqueAttributeUsagePosition) + { + duplicateAttributeUsages.Add($"[{currentAttributeUsage}]"); + } + + for (var j = 0; j (duplicateAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) + { + for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public int Property { get; init; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributes))] + public async Task An_initonly_property_annotated_with_a_unique_parameter_attribute_should_trigger_diagnostic(string attributeName, string attributeUsage) + { + const string propertyIdentifier = "Property"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public int {{propertyIdentifier}} { get; {|#0:init|}; } + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(propertyIdentifier, attributeName); + + await RunAsync(); + } + + public static TheoryData UniqueParameterAttributeUsages => new(UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)); + + public static TheoryData UniqueParameterAttributes => UniqueParameterAttributesTheoryData; + + public static TheoryData DuplicateSameParameterAttributeUsages => DuplicateSameAttributeUsagesTheoryData; + + public static TheoryData DuplicateParameterAttributeUsageCounts => DuplicateAttributeUsageCountsTheoryData; + } +#endif + public class PropertyMustHavePublicSetter : AnalyzerTestFixture + { + public PropertyMustHavePublicSetter() : base(GeneralParameterAttributesAnalyzer.PropertyMustHavePublicSetterRule) { } + + [Theory] + [MemberData(nameof(NonPublicPropertySettersTheoryData))] + public async Task A_property_with_a_nonpublic_setter_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic(string nonPublicPropertySetter) + { + var testCode = /* lang=c#-test */ $$""" + public class BenchmarkClass + { + public int Property {{nonPublicPropertySetter}} + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(NonPublicPropertySettersTheoryData))] + public async Task A_property_with_a_nonpublic_setter_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic(string nonPublicPropertySetter) + { + var testCode = /* lang=c#-test */ $$""" + public class BenchmarkClass + { + [Dummy] + public int Property {{nonPublicPropertySetter}} + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(UniqueParameterAttributeUsages))] + public async Task A_property_with_an_assignable_setter_annotated_with_a_unique_parameter_attribute_should_not_trigger_diagnostic(string attributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attributeUsage}}] + public int Property { get; set; } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_property_with_a_nonpublic_setter_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DuplicateSameParameterAttributeUsages))] (string CurrentUniqueAttributeUsage, int CurrentUniqueAttributeUsagePosition, int[] Counts) duplicateSameParameterAttributeUsages, + [CombinatorialMemberData(nameof(NonPublicPropertySetters))] string nonPublicPropertySetter) + { + var duplicateAttributeUsages = new List(1 + duplicateSameParameterAttributeUsages.Counts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateSameParameterAttributeUsages.Counts.Length; i++) + { + if (i == duplicateSameParameterAttributeUsages.CurrentUniqueAttributeUsagePosition) + { + duplicateAttributeUsages.Add($"[{duplicateSameParameterAttributeUsages.CurrentUniqueAttributeUsage}]"); + } + + for (var j = 0; j < duplicateSameParameterAttributeUsages.Counts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public int Property {{nonPublicPropertySetter}} + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(DuplicateAttributeUsageCountsAndNonPublicPropertySetterCombinations))] + public async Task A_property_with_a_nonpublic_setter_annotated_with_more_than_one_parameter_attribute_should_not_trigger_diagnostic(int[] duplicateAttributeUsageCounts, string nonPublicPropertySetter) + { + var duplicateAttributeUsages = new List(duplicateAttributeUsageCounts.Sum()); + + var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); + + for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) + { + for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) + { + duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); + } + } + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} + public int Property {{nonPublicPropertySetter}} + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_property_with_a_nonpublic_setter_annotated_with_a_unique_parameter_attribute_should_trigger_diagnostic([CombinatorialMemberData(nameof(UniqueParameterAttributes))] (string AttributeName, string AttributeUsage) attribute, + [CombinatorialMemberData(nameof(NonPublicPropertySetters))] string nonPublicPropertySetter) + { + const string propertyIdentifier = "Property"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{attribute.AttributeUsage}}] + public int {|#0:{{propertyIdentifier}}|} {{nonPublicPropertySetter}} + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(propertyIdentifier, attribute.AttributeName); + + await RunAsync(); + } + + public static IEnumerable DuplicateAttributeUsageCountsAndNonPublicPropertySetterCombinations => CombinationsGenerator.CombineArguments(DuplicateParameterAttributeUsageCounts, NonPublicPropertySetters()); + + public static TheoryData UniqueParameterAttributeUsages => new(UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)); + + public static IEnumerable<(string AttributeName, string AttributeUsage)> UniqueParameterAttributes => UniqueParameterAttributesTheoryData.Select(tdr => ((tdr[0] as string)!, (tdr[1] as string)!)); + + public static IEnumerable<(string CurrentUniqueAttributeUsage, int CurrentUniqueAttributeUsagePosition, int[] Counts)> DuplicateSameParameterAttributeUsages => DuplicateSameAttributeUsagesTheoryData.Select(tdr => ((tdr[0] as string)!, (int)tdr[1], (tdr[2] as int[])!)); + + public static TheoryData DuplicateParameterAttributeUsageCounts => DuplicateAttributeUsageCountsTheoryData; + + public static IEnumerable NonPublicPropertySetters() => new NonPublicPropertySetterAccessModifiersTheoryData().Select(m => $"{{ get; {m} set; }}") + .Concat([ + "{ get; }", + "=> 0;" + ]); + public static TheoryData NonPublicPropertySettersTheoryData() => new(NonPublicPropertySetters()); + } + + public static TheoryData UniqueParameterAttributesTheoryData => new() + { + { "Params", "Params(3)" }, + { "ParamsSource", "ParamsSource(\"test\")" }, + { "ParamsAllValues", "ParamsAllValues" } + }; + + public static TheoryData DuplicateSameAttributeUsagesTheoryData + { + get + { + var theoryData = new TheoryData(); + + foreach (var duplicateSameAttributeUsageCombination in GenerateDuplicateSameAttributeUsageCombinations(UniqueParameterAttributesTheoryData)) + { + theoryData.Add(duplicateSameAttributeUsageCombination.CurrentUniqueAttributeUsage, duplicateSameAttributeUsageCombination.CurrentUniqueAttributeUsagePosition, duplicateSameAttributeUsageCombination.Counts); + } + + return theoryData; + } + } + + public static TheoryData DuplicateAttributeUsageCountsTheoryData => new(GenerateDuplicateAttributeUsageCombinations(UniqueParameterAttributesTheoryData)); + + private static IEnumerable GenerateDuplicateAttributeUsageCombinations(TheoryData uniqueAttributeUsages) + { + var uniqueAttributeUsagesList = uniqueAttributeUsages.ToList() + .AsReadOnly(); + + var allCombinations = CombinationsGenerator.GenerateCombinationsCounts(uniqueAttributeUsagesList.Count, 1); + + foreach (var currentCombination in allCombinations) + { + if (currentCombination.Sum() >= 2) + { + yield return currentCombination; + } + } + } + + private static ReadOnlyCollection<(string CurrentUniqueAttributeUsage, int CurrentUniqueAttributeUsagePosition, int[] Counts)> GenerateDuplicateSameAttributeUsageCombinations(TheoryData uniqueAttributeUsages) + { + var uniqueAttributeUsagesList = uniqueAttributeUsages.Select(tdr => (tdr[1] as string)!) + .ToList() + .AsReadOnly(); + + var finalCombinationsList = new List<(string CurrentUniqueAttributeUsage, int CurrentUniqueAttributeUsagePosition, int[] Counts)>(); + + var allCombinations = CombinationsGenerator.GenerateCombinationsCounts(uniqueAttributeUsagesList.Count, 2).ToList() + .AsReadOnly(); + + for (var i = 0; i < uniqueAttributeUsagesList.Count; i++) + { + foreach (var currentCombination in allCombinations) + { + if (currentCombination[i] > 0) + { + finalCombinationsList.Add((uniqueAttributeUsagesList[i], i, currentCombination)); + } + } + } + + return finalCombinationsList.AsReadOnly(); + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAllValuesAttributeAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAllValuesAttributeAnalyzerTests.cs new file mode 100644 index 0000000000..7c97129107 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAllValuesAttributeAnalyzerTests.cs @@ -0,0 +1,258 @@ +namespace BenchmarkDotNet.Analyzers.Tests.AnalyzerTests.Attributes +{ + using Fixtures; + + using BenchmarkDotNet.Analyzers.Attributes; + + using Xunit; + + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + + public class ParamsAllValuesAttributeAnalyzerTests + { + public class General : AnalyzerTestFixture + { + [Theory, CombinatorialData] + public async Task A_field_or_property_not_annotated_with_the_paramsallvalues_attribute_should_not_trigger_diagnostic([CombinatorialValues("", "[Dummy]")] string missingParamsAttributeUsage, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, + [CombinatorialMemberData(nameof(InvalidTypes))] string invalidType) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{missingParamsAttributeUsage}} + public {{invalidType}} {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnumWithFlagsAttribute(); + + await RunAsync(); + } + + public static IEnumerable FieldOrPropertyDeclarations => new FieldOrPropertyDeclarationsTheoryData(); + + public static IEnumerable InvalidTypes => new TheoryData + { + "byte", + "char", + "double", + "float", + "int", + "long", + "sbyte", + "short", + "string", + "uint", + "ulong", + "ushort", + + "DummyEnumWithFlagsAttribute", + + "object", + "System.Type" + }; + } + + public class NotAllowedOnFlagsEnumPropertyOrFieldType : AnalyzerTestFixture + { + public NotAllowedOnFlagsEnumPropertyOrFieldType() : base(ParamsAllValuesAttributeAnalyzer.NotAllowedOnFlagsEnumPropertyOrFieldTypeRule) { } + + [Theory, CombinatorialData] + public async Task A_field_or_property_of_nonnullable_nonenum_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, [CombinatorialMemberData(nameof(NonEnumTypes))] string nonEnumType) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [ParamsAllValues] + public {{nonEnumType}} {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_field_or_property_of_nullable_nonenum_type_should_not_trigger_diagnostic(bool isNullable, [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, [CombinatorialMemberData(nameof(NonEnumStructs))] string nonEnumType) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [ParamsAllValues] + public {{nonEnumType}}{{(isNullable ? "?" : "")}} {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_field_or_property_of_enum_type_without_a_flags_attribute_should_not_trigger_diagnostic(bool isNullable, [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [ParamsAllValues] + public DummyEnum{{(isNullable ? "?" : "")}} {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyEnum(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_field_or_property_of_enum_type_with_a_flags_attribute_should_trigger_diagnostic(bool isNullable, [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + const string enumTypeName = "DummyEnumWithFlagsAttribute"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [ParamsAllValues] + public {|#0:{{enumTypeName}}|}{{(isNullable ? "?" : "")}} {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyEnumWithFlagsAttribute(); + AddDefaultExpectedDiagnostic(enumTypeName); + + await RunAsync(); + } + + public static IEnumerable FieldOrPropertyDeclarations => new FieldOrPropertyDeclarationsTheoryData(); + + public static IEnumerable NonEnumStructs => new List + { + "bool", + "byte", + "char", + "double", + "float", + "int", + "long", + "sbyte", + "short", + "uint", + "ulong", + "ushort", + }.AsReadOnly(); + + public static IEnumerable NonEnumTypes => NonEnumStructs.Concat( + [ + "string", + "object", + "System.Type" + ]); + } + + public class PropertyOrFieldTypeMustBeEnumOrBool : AnalyzerTestFixture + { + public PropertyOrFieldTypeMustBeEnumOrBool() : base(ParamsAllValuesAttributeAnalyzer.PropertyOrFieldTypeMustBeEnumOrBoolRule) { } + + [Theory, CombinatorialData] + public async Task A_field_or_property_of_enum_or_bool_type_should_not_trigger_diagnostic(bool isNullable, [CombinatorialValues("DummyEnum", "bool")] string enumOrBoolType, [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [ParamsAllValues] + public {{enumOrBoolType}}{{(isNullable ? "?" : "")}} {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyEnum(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_field_or_property_not_of_nonnullable_enum_or_bool_type_should_trigger_diagnostic([CombinatorialMemberData(nameof(NonEnumOrBoolTypes))] string nonEnumOrBoolType, [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [ParamsAllValues] + public {|#0:{{nonEnumOrBoolType}}|} {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyEnum(); + AddDefaultExpectedDiagnostic(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_field_or_property_not_of_nullable_enum_or_bool_type_should_trigger_diagnostic(bool isNullable, [CombinatorialMemberData(nameof(NonEnumOrBoolStructs))] string nonEnumOrBoolType, [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [ParamsAllValues] + public {|#0:{{nonEnumOrBoolType}}|}{{(isNullable ? "?" : "")}} {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyEnum(); + AddDefaultExpectedDiagnostic(); + + await RunAsync(); + } + + public static IEnumerable FieldOrPropertyDeclarations => new FieldOrPropertyDeclarationsTheoryData(); + + public static IEnumerable NonEnumOrBoolStructs => new List + { + "byte", + "char", + "double", + "float", + "int", + "long", + "sbyte", + "short", + "uint", + "ulong", + "ushort" + }.AsReadOnly(); + + public static IEnumerable NonEnumOrBoolTypes => NonEnumOrBoolStructs.Concat( + [ + "string", + "object", + "System.Type" + ]); + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs new file mode 100644 index 0000000000..149ecfa04d --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs @@ -0,0 +1,975 @@ +namespace BenchmarkDotNet.Analyzers.Tests.AnalyzerTests.Attributes +{ + using Fixtures; + + using BenchmarkDotNet.Analyzers.Attributes; + + using Xunit; + + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Threading.Tasks; + + public class ParamsAttributeAnalyzerTests + { + public class General : AnalyzerTestFixture + { + [Theory, CombinatorialData] + public async Task A_field_or_property_not_annotated_with_the_params_attribute_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, + [CombinatorialValues("", "[Dummy]")] string missingParamsAttributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{missingParamsAttributeUsage}} + public string {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + public static IEnumerable FieldOrPropertyDeclarations => new FieldOrPropertyDeclarationsTheoryData(); + } + + public class MustHaveValues : AnalyzerTestFixture + { + public MustHaveValues() : base(ParamsAttributeAnalyzer.MustHaveValuesRule) { } + + [Theory, CombinatorialData] + public async Task Providing_one_or_more_values_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesListLength))] int scalarValuesListLength, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgument))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, string.Join(", ", ScalarValues.Take(scalarValuesListLength)))}})] + public string {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_array_with_a_rank_specifier_size_higher_than_zero_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialRange(1, 2)] int rankSpecifierSize) + { + Assert.True(rankSpecifierSize > 0); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params(new object[{{rankSpecifierSize}}])] + public string {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_no_values_should_trigger_diagnostic([CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(EmptyParamsAttributeUsagesWithLocationMarker))] string emptyParamsAttributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}{{emptyParamsAttributeUsage}}] + public string {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + AddDefaultExpectedDiagnostic(); + + await RunAsync(); + } + + public static IEnumerable FieldOrPropertyDeclarations => new FieldOrPropertyDeclarationsTheoryData(); + + public static IEnumerable DummyAttributeUsage => DummyAttributeUsageTheoryData; + + public static IEnumerable ScalarValuesListLength => Enumerable.Range(1, ScalarValues.Count); + + private static ReadOnlyCollection ScalarValues => Enumerable.Range(1, 3) + .Select(i => $"\"test{i}\"") + .ToList() + .AsReadOnly(); + public static IEnumerable ScalarValuesContainerAttributeArgument => ScalarValuesContainerAttributeArgumentTheoryData(); + + public static IEnumerable EmptyParamsAttributeUsagesWithLocationMarker() + { + yield return "{|#0:Params|}"; + yield return "Params{|#0:()|}"; + yield return "Params({|#0:Priority = 1|})"; + + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "Params({0}new object[] {{|#0:{{ }}|}}{1})", + "Params({0}{{|#0:new object[0]|}}{1})", + "Params({0}{{|#0:[ ]|}}{1})", + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + } + } + } + } + } + + public class MustHaveMatchingValueType : AnalyzerTestFixture + { + public MustHaveMatchingValueType() : base(ParamsAttributeAnalyzer.MustHaveMatchingValueTypeRule) { } + + [Theory, CombinatorialData] + public async Task Providing_a_field_or_property_with_an_unknown_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, "42, 51, 33")}})] + public unknown {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_expected_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, valueAndType.Value1)}})] + public {{valueAndType.Value2}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_null_to_nullable_struct_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NullableStructTypes))] string type, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, "null")}})] + public {{type}}? {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_expected_constant_value_type_should_not_trigger_diagnostic(bool useConstantFromOtherClass, + bool useLocalConstant, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ConstantValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{(useLocalConstant ? $"private const {valueAndType.Value2} _x = {(useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1!)};" : "")}} + + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, useLocalConstant ? "_x" : useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1!)}})] + public {{valueAndType.Value2}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + if (useConstantFromOtherClass) + { + ReferenceConstants($"{valueAndType.Value2!}", valueAndType.Value1!); + } + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_expected_null_reference_constant_value_type_should_not_trigger_diagnostic(bool useConstantFromOtherClass, + bool useLocalConstant, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NullReferenceConstantTypes))] string type, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{(useLocalConstant ? $"private const {type}? _x = {(useConstantFromOtherClass ? "Constants.Value" : "null")};" : "")}} + + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, useLocalConstant ? "_x" : useConstantFromOtherClass ? "Constants.Value" : "null")}})] + public {{type}}? {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + if (useConstantFromOtherClass) + { + ReferenceConstants($"{type}?", "null"); + } + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_implicitly_convertible_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialValues("(byte)42", "'c'")] string value, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, value)}})] + public int {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_integer_value_types_within_target_type_range_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(IntegerValuesAndTypesWithinTargetTypeRange))] ValueTupleDouble integerValueAndType, + bool explicitCast, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, $"{(explicitCast ? $"({integerValueAndType.Value2})" : "")}{integerValueAndType.Value1}")}})] + public {{integerValueAndType.Value2}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_unknown_value_type_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + const string expectedFieldOrPropertyType = "int"; + const string valueWithUnknownType = "dummy_literal"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, $"42, {{|#0:{valueWithUnknownType}|}}, 33")}})] + public {{expectedFieldOrPropertyType}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_both_expected_and_unexpected_value_types_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + const string expectedFieldOrPropertyType = "int"; + const string valueWithUnexpectedType = "\"test1\""; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, $"42, {{|#0:{valueWithUnexpectedType}|}}, 33")}})] + public {{expectedFieldOrPropertyType}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + + AddDefaultExpectedDiagnostic(valueWithUnexpectedType, expectedFieldOrPropertyType, "string"); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_unexpected_or_not_implicitly_convertible_value_type_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NotConvertibleValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + const string expectedFieldOrPropertyType = "decimal"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, $"{{|#0:{valueAndType.Value1}|}}")}})] + public {{expectedFieldOrPropertyType}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + AddDefaultExpectedDiagnostic(valueAndType.Value1!, expectedFieldOrPropertyType, valueAndType.Value2!); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_unexpected_or_not_implicitly_convertible_constant_value_type_should_trigger_diagnostic(bool useConstantFromOtherClass, + bool useLocalConstant, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NotConvertibleConstantValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + const string expectedFieldOrPropertyType = "decimal"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + {{(useLocalConstant ? $"private const {valueAndType.Value2} _x = {(useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1)};" : "")}} + + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, $"{{|#0:{(useLocalConstant ? "_x" : useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1)}|}}")}})] + public {{expectedFieldOrPropertyType}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + if (useConstantFromOtherClass) + { + ReferenceConstants(valueAndType.Value2!, valueAndType.Value1!); + } + + AddDefaultExpectedDiagnostic(useLocalConstant ? "_x" : useConstantFromOtherClass ? "Constants.Value" : valueAndType.Value1!, expectedFieldOrPropertyType, valueAndType.Value2!); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_null_to_nonnullable_struct_value_type_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(NullableStructTypes))] string type, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, "{|#0:null|}")}})] + public {{type}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + AddDefaultExpectedDiagnostic("null", type, "null"); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_integer_value_types_not_within_target_type_range_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentEnumerable))] string scalarValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(IntegerValuesAndTypesNotWithinTargetTypeRange))] ValueTupleTriple integerValueAndType, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, $"{{|#0:{integerValueAndType.Value1}|}}")}})] + public {{integerValueAndType.Value2}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + AddDefaultExpectedDiagnostic(integerValueAndType.Value1!, integerValueAndType.Value2!, integerValueAndType.Value3!); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_unexpected_array_value_type_to_params_attribute_should_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ValuesAndTypes))] ValueTupleDouble valueAndType, + [CombinatorialMemberData(nameof(ArrayValuesContainerAttributeArgumentWithLocationMarkerEnumerable))] ValueTupleDouble arrayValuesContainerAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + const string expectedFieldOrPropertyType = "decimal"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(arrayValuesContainerAttributeArgument.Value1, valueAndType.Value1, valueAndType.Value2)}})] + public {{expectedFieldOrPropertyType}} {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + ReferenceDummyEnum(); + + AddDefaultExpectedDiagnostic(string.Format(arrayValuesContainerAttributeArgument.Value2, + valueAndType.Value1, + valueAndType.Value2), + expectedFieldOrPropertyType, + $"{valueAndType.Value2}[]"); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_empty_array_value_when_type_of_attribute_target_is_not_object_array_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(EmptyValuesAttributeArgument))] string emptyValuesAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params{{emptyValuesAttributeArgument}}] + public decimal {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_empty_array_value_when_type_of_attribute_target_is_object_array_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(EmptyValuesAttributeArgument))] string emptyValuesAttributeArgument, + [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params{{emptyValuesAttributeArgument}}] + public object[] {{fieldOrPropertyDeclaration}} + } + """; + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + public static IEnumerable FieldOrPropertyDeclarations => new FieldOrPropertyDeclarationsTheoryData(); + + public static IEnumerable DummyAttributeUsage => DummyAttributeUsageTheoryData; + + public static IEnumerable> IntegerValuesAndTypesWithinTargetTypeRange => + [ + // byte (0 to 255) + ("0", "byte"), + ("100", "byte"), + ("255", "byte"), + + // sbyte (-128 to 127) + ("-128", "sbyte"), + ("0", "sbyte"), + ("127", "sbyte"), + + // short (-32,768 to 32,767) + ("-32768", "short"), + ("0", "short"), + ("32767", "short"), + + // ushort (0 to 65,535) + ("0", "ushort"), + ("1000", "ushort"), + ("65535", "ushort"), + + // int (-2,147,483,648 to 2,147,483,647) + ("-2147483648", "int"), + ("0", "int"), + ("2147483647", "int"), + + // uint (0 to 4,294,967,295) + ("0", "uint"), + ("1000000", "uint"), + ("4294967295", "uint"), + + // long (-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807) + ("-9223372036854775808", "long"), + ("0", "long"), + ("9223372036854775807", "long"), + + // ulong (0 to 18,446,744,073,709,551,615) + ("0", "ulong"), + ("1000000", "ulong"), + ("18446744073709551615", "ulong"), + ]; + + public static IEnumerable> IntegerValuesAndTypesNotWithinTargetTypeRange => + [ + // byte (0 to 255) - out of range values + ("-1", "byte", "int"), + ("256", "byte", "int"), + ("1000", "byte", "int"), + + // sbyte (-128 to 127) - out of range values + ("-129", "sbyte", "int"), + ("128", "sbyte", "int"), + ("500", "sbyte", "int"), + + // short (-32,768 to 32,767) - out of range values + ("-32769", "short", "int"), + ("32768", "short", "int"), + ("100000", "short", "int"), + + // ushort (0 to 65,535) - out of range values + ("-1", "ushort", "int"), + ("65536", "ushort", "int"), + ("100000", "ushort", "int"), + + // int (-2,147,483,648 to 2,147,483,647) - out of range values + ("-2147483649", "int", "long"), + ("2147483648", "int", "uint"), + ("5000000000", "int", "long"), + + // uint (0 to 4,294,967,295) - out of range values + ("-1", "uint", "int"), + ("4294967296", "uint", "long"), + ("5000000000", "uint", "long"), + + // long - out of range values (exceeding long range) + ("9223372036854775808", "long", "ulong"), + + // ulong - negative values + ("-1", "ulong", "int"), + ("-100", "ulong", "int"), + ("-9223372036854775808", "ulong", "long"), + ]; + + public static IEnumerable EmptyValuesAttributeArgument() + { + yield return ""; + yield return "()"; + yield return "(Priority = 1)"; + + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "({0}new object[] {{ }}{1})", + "({0}new object[0]{1})", + "({0}[ ]{1})" + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + } + } + } + } + + public static IEnumerable ScalarValuesContainerAttributeArgumentEnumerable => ScalarValuesContainerAttributeArgumentTheoryData(); + + public static IEnumerable> ArrayValuesContainerAttributeArgumentWithLocationMarkerEnumerable() + { + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List<(string, string)> + { + ("{0}new object[] {{{{ {{{{|#0:new[] {{{{ {{0}} }}}}|}}}} }}}}{1}", "new[] {{ {0} }}"), // new object[] { new[] { 42 } } + ("{0}new object[] {{{{ {{{{|#0:new {{1}}[] {{{{ {{0}} }}}}|}}}} }}}}{1}", "new {1}[] {{ {0} }}"), // new object[] { new int[] { 42 } } + ("{0}[ {{{{|#0:new[] {{{{ {{0}} }}}}|}}}} ]{1}", "new[] {{ {0} }}"), // [ new[] { 42 } ] + ("{0}[ {{{{|#0:new {{1}}[] {{{{ {{0}} }}}}|}}}} ]{1}", "new {1}[] {{ {0} }}"), // [ new int[] { 42 } ] + ("{0}new object[] {{{{ {{{{|#0:new {{1}}[] {{{{ }}}}|}}}} }}}}{1}", "new {1}[] {{ }}"), // new object[] { new int[] { } } + ("{0}[ {{{{|#0:new {{1}}[] {{{{ }}}}|}}}} ]{1}", "new {1}[] {{ }}"), // [ new int[] { } ] + ("{0}new object[] {{{{ {{{{|#0:new {{1}}[0]|}}}} }}}}{1}", "new {1}[0]"), // new object[] { new int[0] } + ("{0}[ {{{{|#0:new {{1}}[0]|}}}} ]{1}", "new {1}[0]"), // [ new int[0] ] + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return new ValueTupleDouble + { + Value1 = string.Format(attributeUsageBase.Item1, nameColonUsage, priorityNamedParameterUsage), + Value2 = attributeUsageBase.Item2 + }; + } + } + } + } + + public static IEnumerable> ValuesAndTypes => + [ + ( "true", "bool" ), + ( "(byte)123", "byte" ), + ( "'A'", "char" ), + ( "1.0D", "double" ), + ( "1.0F", "float" ), + ( "123", "int" ), + ( "123L", "long" ), + ( "(sbyte)-100", "sbyte" ), + ( "(short)-123", "short" ), + ( """ + "test" + """, "string" ), + ( "123U", "uint" ), + ( "123UL", "ulong" ), + ( "(ushort)123", "ushort" ), + + ( """ + (object)"test_object" + """, "object" ), + ( "typeof(string)", "System.Type" ), + ( "DummyEnum.Value1", "DummyEnum" ), + ]; + + public static IEnumerable> NotConvertibleValuesAndTypes => + [ + ( "true", "bool" ), + ( "1.0D", "double" ), + ( "1.0F", "float" ), + ( """ + "test" + """, "string" ), + + ( """ + (object)"test_object" + """, "object" ), + ( "typeof(string)", "System.Type" ), + ( "DummyEnum.Value1", "DummyEnum" ) + ]; + + public static IEnumerable> ConstantValuesAndTypes => + [ + ( "true", "bool" ), + ( "(byte)123", "byte" ), + ( "'A'", "char" ), + ( "1.0D", "double" ), + ( "1.0F", "float" ), + ( "123", "int" ), + ( "123L", "long" ), + ( "(sbyte)-100", "sbyte" ), + ( "(short)-123", "short" ), + ( """ + "test" + """, "string" ), + ( "123U", "uint" ), + ( "123UL", "ulong" ), + ( "(ushort)123", "ushort" ), + + ( "DummyEnum.Value1", "DummyEnum" ), + ]; + + public static IEnumerable NullableStructTypes => + [ + "bool", + "byte", + "char", + "double", + "float", + "int", + "long", + "sbyte", + "short", + "uint", + "ulong", + "ushort", + "DummyEnum", + ]; + + public static IEnumerable NullReferenceConstantTypes => + [ + "object", + "string", + "System.Type", + ]; + + public static IEnumerable> NotConvertibleConstantValuesAndTypes => + [ + ( "true", "bool" ), + ( "1.0D", "double" ), + ( "1.0F", "float" ), + ( """ + "test" + """, "string" ), + + ( "DummyEnum.Value1", "DummyEnum" ) + ]; + } + + public class UnnecessarySingleValuePassedToAttribute : AnalyzerTestFixture + { + public UnnecessarySingleValuePassedToAttribute() : base(ParamsAttributeAnalyzer.UnnecessarySingleValuePassedToAttributeRule) { } + + [Theory, CombinatorialData] + public async Task Providing_two_or_more_values_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesListLength))] int scalarValuesListLength, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgument))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, string.Join(", ", ScalarValues.Take(scalarValuesListLength)))}})] + public string {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_only_a_single_value_should_trigger_diagnostic([CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, + [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, + [CombinatorialMemberData(nameof(ScalarValuesContainerAttributeArgumentWithLocationMarker))] string scalarValuesContainerAttributeArgument) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [{{dummyAttributeUsage}}Params({{string.Format(scalarValuesContainerAttributeArgument, 42)}})] + public string {{fieldOrPropertyDeclaration}} + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + AddDefaultExpectedDiagnostic(); + + await RunAsync(); + } + + public static IEnumerable FieldOrPropertyDeclarations => new FieldOrPropertyDeclarationsTheoryData(); + + public static IEnumerable DummyAttributeUsage => DummyAttributeUsageTheoryData; + + public static IEnumerable ScalarValuesListLength => Enumerable.Range(2, ScalarValues.Count); + + private static ReadOnlyCollection ScalarValues => Enumerable.Range(1, 2) + .Select(i => $"\"test{i}\"") + .ToList() + .AsReadOnly(); + public static IEnumerable ScalarValuesContainerAttributeArgument => ScalarValuesContainerAttributeArgumentTheoryData(); + + public static IEnumerable ScalarValuesContainerAttributeArgumentWithLocationMarker() + { + return GenerateData().Distinct(); + + static IEnumerable GenerateData() + { + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "{{{{|#0:{{0}}|}}}}{1}", + "{0}new object[] {{{{ {{{{|#0:{{0}}|}}}} }}}}{1}", + "{0}[ {{{{|#0:{{0}}|}}}} ]{1}", + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + } + } + } + } + } + } + + public static TheoryData DummyAttributeUsageTheoryData => [ + "", + "Dummy, " + ]; + + public static TheoryData ScalarValuesContainerAttributeArgumentTheoryData() + { + return new TheoryData(GenerateData().Distinct()); + + static IEnumerable GenerateData() + { + var nameColonUsages = new List + { + "", + "values: " + }; + + var priorityNamedParameterUsages = new List + { + "", + ", Priority = 1" + }; + + var attributeUsagesBase = new List + { + "{{0}}{1}", + "{0}new object[] {{{{ {{0}} }}}}{1}", + "{0}[ {{0}} ]{1}" + }; + + foreach (var attributeUsageBase in attributeUsagesBase) + { + foreach (var nameColonUsage in nameColonUsages) + { + foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) + { + yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + } + } + } + } + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/BenchmarkRunner/RunAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/BenchmarkRunner/RunAnalyzerTests.cs new file mode 100644 index 0000000000..34f49bfbeb --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/BenchmarkRunner/RunAnalyzerTests.cs @@ -0,0 +1,903 @@ +namespace BenchmarkDotNet.Analyzers.Tests.AnalyzerTests.BenchmarkRunner +{ + using Fixtures; + + using Analyzers.BenchmarkRunner; + + using Xunit; + + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Threading.Tasks; + + public class RunAnalyzerTests + { + public class General : AnalyzerTestFixture + { + [Theory, CombinatorialData] + public async Task Invoking_with_a_public_nonabstract_unsealed_nongeneric_type_argument_class_having_only_one_and_public_method_annotated_with_the_benchmark_attribute_should_not_trigger_diagnostic(bool isGenericInvocation) + { + const string classWithOneBenchmarkMethodName = "ClassWithOneBenchmarkMethod"; + + var invocationExpression = isGenericInvocation ? $"<{classWithOneBenchmarkMethodName}>()" : $"(typeof({classWithOneBenchmarkMethodName}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + """; + + const string benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class {{classWithOneBenchmarkMethodName}} + { + [Benchmark] + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + + await RunAsync(); + } + } + + public class TypeArgumentClassMissingBenchmarkMethods : AnalyzerTestFixture + { + public TypeArgumentClassMissingBenchmarkMethods() : base(RunAnalyzer.TypeArgumentClassMissingBenchmarkMethodsRule) { } + + [Theory, CombinatorialData] + public async Task Invoking_with_type_argument_class_having_at_least_one_public_method_annotated_with_the_benchmark_attribute_should_not_trigger_diagnostic(bool isGenericInvocation) + { + const string classWithOneBenchmarkMethodName = "ClassWithOneBenchmarkMethod"; + + var invocationExpression = isGenericInvocation ? $"<{classWithOneBenchmarkMethodName}>()" : $"(typeof({classWithOneBenchmarkMethodName}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + """; + + const string benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class {{classWithOneBenchmarkMethodName}} + { + [Benchmark] + public void BenchmarkMethod() + { + + } + + public void BenchmarkMethod2() + { + + } + + private void BenchmarkMethod3() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_type_argument_class_having_at_least_one_public_method_annotated_with_the_benchmark_attribute_in_one_of_its_ancestor_classes_should_not_trigger_diagnostic(bool isGenericInvocation, [CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier) + { + const string classWithOneBenchmarkMethodName = "TopLevelBenchmarkClass"; + + var invocationExpression = isGenericInvocation ? $"<{classWithOneBenchmarkMethodName}>()" : $"(typeof({classWithOneBenchmarkMethodName}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + """; + + const string benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class {{classWithOneBenchmarkMethodName}} : BenchmarkClassAncestor1 + { + } + """; + + var benchmarkClassAncestor1Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor1 : BenchmarkClassAncestor2 + { + } + """; + + var benchmarkClassAncestor2Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor2 : BenchmarkClassAncestor3 + { + } + """; + + var benchmarkClassAncestor3Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor3 + { + [Benchmark] + public void BenchmarkMethod() + { + + } + + public void BenchmarkMethod2() + { + + } + + private void BenchmarkMethod3() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddSource(benchmarkClassAncestor1Document); + AddSource(benchmarkClassAncestor2Document); + AddSource(benchmarkClassAncestor3Document); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_generic_type_argument_class_having_at_least_one_public_method_annotated_with_the_benchmark_attribute_in_one_of_its_ancestor_classes_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(TypeParametersListLengthEnumerableLocal))] int typeParametersListLength, + [CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier) + { + var typeParameters = string.Join(", ", TypeParameters.Take(typeParametersListLength)); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run(typeof(BenchmarkClass<{{new string(',', typeParametersListLength - 1)}}>)); + } + } + """; + + var benchmarkClassDocument = /* lang=c#-test */ $$""" + public class BenchmarkClass<{{typeParameters}}> : BenchmarkClassAncestor1<{{typeParameters}}> + { + } + """; + + var benchmarkClassAncestor1Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor1<{{typeParameters}}> : BenchmarkClassAncestor2<{{typeParameters}}> + { + } + """; + + var benchmarkClassAncestor2Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor2<{{typeParameters}}> : BenchmarkClassAncestor3 + { + } + """; + + var benchmarkClassAncestor3Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor3 + { + [Benchmark] + public void BenchmarkMethod() + { + + } + + public void BenchmarkMethod2() + { + + } + + private void BenchmarkMethod3() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddSource(benchmarkClassAncestor1Document); + AddSource(benchmarkClassAncestor2Document); + AddSource(benchmarkClassAncestor3Document); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_type_argument_class_having_no_public_method_annotated_with_the_benchmark_attribute_in_one_of_its_ancestor_classes_should_trigger_diagnostic(bool isGenericInvocation, [CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier) + { + const string classWithOneBenchmarkMethodName = "TopLevelBenchmarkClass"; + + var invocationExpression = isGenericInvocation ? $"<{{|#0:{classWithOneBenchmarkMethodName}|}}>()" : $"(typeof({{|#0:{classWithOneBenchmarkMethodName}|}}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + """; + + const string benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class {{classWithOneBenchmarkMethodName}} : BenchmarkClassAncestor1 + { + } + """; + + var benchmarkClassAncestor1Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor1 : BenchmarkClassAncestor2 + { + } + """; + + var benchmarkClassAncestor2Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor2 : BenchmarkClassAncestor3 + { + } + """; + + var benchmarkClassAncestor3Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor3 + { + public void BenchmarkMethod() + { + + } + + public void BenchmarkMethod2() + { + + } + + private void BenchmarkMethod3() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddSource(benchmarkClassAncestor1Document); + AddSource(benchmarkClassAncestor2Document); + AddSource(benchmarkClassAncestor3Document); + + AddDefaultExpectedDiagnostic(classWithOneBenchmarkMethodName); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_generic_type_argument_class_having_no_public_method_annotated_with_the_benchmark_attribute_in_one_of_its_ancestor_classes_should_trigger_diagnostic([CombinatorialMemberData(nameof(TypeParametersListLengthEnumerableLocal))] int typeParametersListLength, + [CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier) + { + const string classWithOneBenchmarkMethodName = "BenchmarkClass"; + + var unboundGenericTypeParameterList = new string(',', typeParametersListLength - 1); + var typeParameters = string.Join(", ", TypeParameters.Take(typeParametersListLength)); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run(typeof({|#0:{{classWithOneBenchmarkMethodName}}<{{unboundGenericTypeParameterList}}>|})); + } + } + """; + + var benchmarkClassDocument = /* lang=c#-test */ $$""" + public class {{classWithOneBenchmarkMethodName}}<{{typeParameters}}> : BenchmarkClassAncestor1<{{typeParameters}}> + { + } + """; + + var benchmarkClassAncestor1Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor1<{{typeParameters}}> : BenchmarkClassAncestor2<{{typeParameters}}> + { + } + """; + + var benchmarkClassAncestor2Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor2<{{typeParameters}}> : BenchmarkClassAncestor3 + { + } + """; + + var benchmarkClassAncestor3Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor3 + { + public void BenchmarkMethod() + { + + } + + private void BenchmarkMethod2() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddSource(benchmarkClassAncestor1Document); + AddSource(benchmarkClassAncestor2Document); + AddSource(benchmarkClassAncestor3Document); + + AddDefaultExpectedDiagnostic($"{classWithOneBenchmarkMethodName}<{unboundGenericTypeParameterList}>"); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_type_argument_class_having_no_public_method_annotated_with_the_benchmark_attribute_should_trigger_diagnostic(bool isGenericInvocation) + { + const string classWithOneBenchmarkMethodName = "ClassWithOneBenchmarkMethod"; + + var invocationExpression = isGenericInvocation ? $"<{{|#0:{classWithOneBenchmarkMethodName}|}}>()" : $"(typeof({{|#0:{classWithOneBenchmarkMethodName}|}}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + """; + + const string benchmarkClassDocument = /* lang=c#-test */ $$""" + public class {{classWithOneBenchmarkMethodName}} + { + public void BenchmarkMethod() + { + + } + } + """; + TestCode = testCode; + AddSource(benchmarkClassDocument); + + AddDefaultExpectedDiagnostic(classWithOneBenchmarkMethodName); + + await RunAsync(); + } + + public static IEnumerable ClassAbstractModifiersEnumerableLocal => ClassAbstractModifiersEnumerable; + + public static IEnumerable TypeParametersListLengthEnumerableLocal => TypeParametersListLengthEnumerable; + } + + public class TypeArgumentClassMustBePublic : AnalyzerTestFixture + { + public TypeArgumentClassMustBePublic() : base(RunAnalyzer.TypeArgumentClassMustBePublicRule) { } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_nonpublic_class_with_multiple_inheritance_containing_at_least_one_method_annotated_with_benchmark_attribute_should_trigger_diagnostic(bool isGenericInvocation) + { + const string benchmarkClassName = "BenchmarkClass"; + + var invocationExpression = isGenericInvocation ? $"<{{|#0:{benchmarkClassName}|}}>()" : $"(typeof({{|#0:{benchmarkClassName}|}}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + + private class {{benchmarkClassName}} : BenchmarkClassAncestor1 + { + } + + private class BenchmarkClassAncestor1 : BenchmarkClassAncestor2 + { + [Benchmark] + public void BenchmarkMethod() + { + + } + } + } + """; + + const string benchmarkClassAncestor2Document = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClassAncestor2 + { + } + """; + + + + TestCode = testCode; + AddSource(benchmarkClassAncestor2Document); + + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_nonpublic_class_containing_at_least_one_method_annotated_with_benchmark_attribute_should_trigger_diagnostic([CombinatorialMemberData(nameof(NonPublicClassAccessModifiersExceptFile))] string nonPublicClassAccessModifier, + bool isGenericInvocation) + { + const string benchmarkClassName = "BenchmarkClass"; + + var invocationExpression = isGenericInvocation ? $"<{{|#0:{benchmarkClassName}|}}>()" : $"(typeof({{|#0:{benchmarkClassName}|}}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + + {{nonPublicClassAccessModifier}}class {{benchmarkClassName}} + { + [Benchmark] + public void BenchmarkMethod() + { + + } + } + } + """; + + TestCode = testCode; + + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_file_class_containing_at_least_one_method_annotated_with_benchmark_attribute_should_trigger_diagnostic(bool isGenericInvocation) + { + const string benchmarkClassName = "BenchmarkClass"; + + var invocationExpression = isGenericInvocation ? $"<{{|#0:{benchmarkClassName}|}}>()" : $"(typeof({{|#0:{benchmarkClassName}|}}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + + file class {{benchmarkClassName}} + { + [Benchmark] + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + + public static IEnumerable NonPublicClassAccessModifiersExceptFile => new NonPublicClassAccessModifiersTheoryData().Where(m => m != "file "); + } + + public class TypeArgumentClassMustBeUnsealed : AnalyzerTestFixture + { + public TypeArgumentClassMustBeUnsealed() : base(RunAnalyzer.TypeArgumentClassMustBeUnsealedRule) { } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_sealed_benchmark_class_should_trigger_diagnostic(bool isGenericInvocation) + { + const string benchmarkClassName = "BenchmarkClass"; + + var invocationExpression = isGenericInvocation ? $"<{{|#0:{benchmarkClassName}|}}>()" : $"(typeof({{|#0:{benchmarkClassName}|}}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + """; + + const string benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public sealed class {{benchmarkClassName}} + { + [Benchmark] + public void BenchmarkMethod() + { + + } + } + """; + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + } + + public class TypeArgumentClassMustBeNonAbstract : AnalyzerTestFixture + { + public TypeArgumentClassMustBeNonAbstract() : base(RunAnalyzer.TypeArgumentClassMustBeNonAbstractRule) { } + + [Theory, CombinatorialData] + public async Task Invoking_with_an_abstract_benchmark_class_should_trigger_diagnostic(bool isGenericInvocation) + { + const string benchmarkClassName = "BenchmarkClass"; + + var invocationExpression = isGenericInvocation ? $"<{{|#0:{benchmarkClassName}|}}>()" : $"(typeof({{|#0:{benchmarkClassName}|}}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + """; + + const string benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public abstract class {{benchmarkClassName}} + { + [Benchmark] + public void BenchmarkMethod() + { + + } + } + """; + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + } + + public class GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute : AnalyzerTestFixture + { + public GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute() : base(RunAnalyzer.GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttributeRule) { } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_generic_class_annotated_with_at_least_one_generictypearguments_attribute_should_not_trigger_diagnostic([CombinatorialRange(1, 2)] int genericTypeArgumentsAttributeUsageCount, + [CombinatorialMemberData(nameof(TypeParametersListLengthEnumerableLocal))] int typeParametersListLength, + [CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier, + [CombinatorialMemberData(nameof(BenchmarkAttributeUsagesEnumerableLocal))] string benchmarkAttributeUsage) + { + const string benchmarkClassName = "BenchmarkClass"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run(typeof({{benchmarkClassName}}<{{new string(',', typeParametersListLength - 1)}}>)); + } + } + """; + + var genericTypeArguments = string.Join(", ", GenericTypeArguments.Select(ta => $"typeof({ta})").Take(typeParametersListLength)); + var genericTypeArgumentsAttributeUsages = string.Join("\n", Enumerable.Repeat($"[GenericTypeArguments({genericTypeArguments})]", genericTypeArgumentsAttributeUsageCount)); + var typeParameters = string.Join(", ", TypeParameters.Take(typeParametersListLength)); + + var benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + {{genericTypeArgumentsAttributeUsages}} + public class BenchmarkClass<{{typeParameters}}> : BenchmarkClassAncestor1<{{typeParameters}}> + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + var benchmarkClassAncestor1Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor1<{{typeParameters}}> : BenchmarkClassAncestor2<{{typeParameters}}> + { + + } + """; + var benchmarkClassAncestor2Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor2<{{typeParameters}}> + { + + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddSource(benchmarkClassAncestor1Document); + AddSource(benchmarkClassAncestor2Document); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_nongeneric_class_that_inherits_from_a_generic_class_not_annotated_with_a_generictypearguments_attribute_should_not_trigger_diagnostic(bool isGenericInvocation, + [CombinatorialMemberData(nameof(TypeParametersListLengthEnumerableLocal))] int typeParametersListLength, + [CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier, + [CombinatorialMemberData(nameof(BenchmarkAttributeUsagesEnumerableLocal))] string benchmarkAttributeUsage) + { + const string benchmarkClassName = "BenchmarkClass"; + + var invocationExpression = isGenericInvocation ? $"<{benchmarkClassName}>()" : $"(typeof({benchmarkClassName}))"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run{{invocationExpression}}; + } + } + """; + + var benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class {{benchmarkClassName}} : BenchmarkClassAncestor<{{string.Join(", ", GenericTypeArguments.Take(typeParametersListLength))}}> + { + + } + """; + var benchmarkClassAncestorDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor<{{string.Join(", ", TypeParameters.Take(typeParametersListLength))}}> + { + + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddSource(benchmarkClassAncestorDocument); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task A_generic_class_not_referenced_in_run_method_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier, + [CombinatorialMemberData(nameof(TypeParametersListLengthEnumerableLocal))] int typeParametersListLength, + [CombinatorialMemberData(nameof(BenchmarkAttributeUsagesEnumerableLocal))] string benchmarkAttributeUsage) + { + var typeParameters = string.Join(", ", TypeParameters.Take(typeParametersListLength)); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass<{{string.Join(", ", TypeParameters.Take(typeParametersListLength))}}> : BenchmarkClassAncestor1<{{typeParameters}}> + { + } + """; + + var benchmarkClassAncestor1Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor1<{{typeParameters}}> : BenchmarkClassAncestor2<{{typeParameters}}> + { + } + """; + + var benchmarkClassAncestor2Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor2<{{typeParameters}}> : BenchmarkClassAncestor3 + { + } + """; + + var benchmarkClassAncestor3Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {{abstractModifier}}class BenchmarkClassAncestor3 + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + + public void BenchmarkMethod2() + { + + } + + private void BenchmarkMethod3() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassAncestor1Document); + AddSource(benchmarkClassAncestor2Document); + AddSource(benchmarkClassAncestor3Document); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Invoking_with_a_generic_class_not_annotated_with_a_generictypearguments_attribute_should_trigger_diagnostic([CombinatorialRange(0, 3)] int genericTypeArgumentsAttributeUsageCount, + [CombinatorialMemberData(nameof(TypeParametersListLengthEnumerableLocal))] int typeParametersListLength, + [CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier, + [CombinatorialMemberData(nameof(BenchmarkAttributeUsagesEnumerableLocal))] string benchmarkAttributeUsage) + { + const string benchmarkClassName = "BenchmarkClass"; + + var unboundGenericTypeParameterList = new string(',', typeParametersListLength - 1); + var typeParameters = string.Join(", ", TypeParameters.Take(typeParametersListLength)); + var genericTypeArguments = string.Join(", ", GenericTypeArguments.Select(ta => $"typeof({ta})").Take(typeParametersListLength)); + var genericTypeArgumentsAttributeUsages = string.Join("\n", Enumerable.Repeat($"[GenericTypeArguments({genericTypeArguments})]", genericTypeArgumentsAttributeUsageCount)); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Running; + + public class Program + { + public static void Main(string[] args) { + BenchmarkRunner.Run(typeof({|#0:{{benchmarkClassName}}<{{unboundGenericTypeParameterList}}>|})); + } + } + """; + + var benchmarkClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class {{benchmarkClassName}}<{{typeParameters}}> : BenchmarkClassAncestor1<{{typeParameters}}> + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + var benchmarkClassAncestor1Document = /* lang=c#-test */ $$""" + public {{abstractModifier}}class BenchmarkClassAncestor1<{{typeParameters}}> : BenchmarkClassAncestor2<{{typeParameters}}> + { + + } + """; + + var benchmarkClassAncestor2Document = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + {{genericTypeArgumentsAttributeUsages}} + public {{abstractModifier}}class BenchmarkClassAncestor2<{{typeParameters}}> + { + + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkClassDocument); + AddSource(benchmarkClassAncestor1Document); + AddSource(benchmarkClassAncestor2Document); + + AddDefaultExpectedDiagnostic($"{benchmarkClassName}<{unboundGenericTypeParameterList}>"); + + await RunAsync(); + } + + public static IEnumerable ClassAbstractModifiersEnumerableLocal => ClassAbstractModifiersEnumerable; + + public static IEnumerable BenchmarkAttributeUsagesEnumerableLocal => BenchmarkAttributeUsagesEnumerable; + + public static IEnumerable TypeParametersListLengthEnumerableLocal => TypeParametersListLengthEnumerable; + } + + public static IEnumerable ClassAbstractModifiersEnumerable => [ "", "abstract " ]; + + public static IEnumerable BenchmarkAttributeUsagesEnumerable => [ "", "[Benchmark]" ]; + + public static IEnumerable TypeParametersListLengthEnumerable => Enumerable.Range(1, TypeParameters.Count); + + private static ReadOnlyCollection TypeParameters => Enumerable.Range(1, 3) + .Select(i => $"TParameter{i}") + .ToList() + .AsReadOnly(); + + private static ReadOnlyCollection GenericTypeArguments => new List { "int", "string", "bool" }.AsReadOnly(); + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/General/BenchmarkClassAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/General/BenchmarkClassAnalyzerTests.cs new file mode 100644 index 0000000000..51736dcde2 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/General/BenchmarkClassAnalyzerTests.cs @@ -0,0 +1,567 @@ +namespace BenchmarkDotNet.Analyzers.Tests.AnalyzerTests.General +{ + using Fixtures; + + using Analyzers.General; + + using Xunit; + + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Threading.Tasks; + + public class BenchmarkClassAnalyzerTests + { + public class General : AnalyzerTestFixture + { + [Theory] + [InlineData("")] + [InlineData(" abstract")] + public async Task Class_not_annotated_with_any_generictypearguments_attributes_and_with_no_methods_annotated_with_benchmark_attribute_should_not_trigger_diagnostic(string abstractModifier) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public{{abstractModifier}} class BenchmarkClass + { + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + } + + public class ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract : AnalyzerTestFixture + { + public ClassWithGenericTypeArgumentsAttributeMustBeNonAbstract() : base(BenchmarkClassAnalyzer.ClassWithGenericTypeArgumentsAttributeMustBeNonAbstractRule) { } + + [Theory, CombinatorialData] + public async Task Abstract_class_annotated_with_at_least_one_generictypearguments_attribute_should_trigger_diagnostic([CombinatorialRange(1, 2)] int genericTypeArgumentsAttributeUsageCount, [CombinatorialValues("", "[Benchmark]")] string benchmarkAttributeUsage) + { + const string benchmarkClassName = "BenchmarkClass"; + + var genericTypeArgumentsAttributeUsages = Enumerable.Repeat("[GenericTypeArguments(typeof(int))]", genericTypeArgumentsAttributeUsageCount); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + {{string.Join("\n", genericTypeArgumentsAttributeUsages)}} + public {|#0:abstract|} class {{benchmarkClassName}} + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + } + + public class ClassWithGenericTypeArgumentsAttributeMustBeGeneric : AnalyzerTestFixture + { + public ClassWithGenericTypeArgumentsAttributeMustBeGeneric() : base(BenchmarkClassAnalyzer.ClassWithGenericTypeArgumentsAttributeMustBeGenericRule) { } + + [Theory, CombinatorialData] + public async Task Generic_class_annotated_with_a_generictypearguments_attribute_should_not_trigger_diagnostic([CombinatorialRange(1, 2)] int genericTypeArgumentsAttributeUsageCount, + [CombinatorialValues("", "[Benchmark]")] string benchmarkAttributeUsage) + { + var genericTypeArgumentsAttributeUsages = Enumerable.Repeat("[GenericTypeArguments(typeof(int))]", genericTypeArgumentsAttributeUsageCount); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + {{string.Join("\n", genericTypeArgumentsAttributeUsages)}} + public class BenchmarkClass + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Nongeneric_class_annotated_with_a_generictypearguments_attribute_should_trigger_diagnostic([CombinatorialRange(1, 2)] int genericTypeArgumentsAttributeUsageCount, + [CombinatorialValues("", "[Benchmark]")] string benchmarkAttributeUsage) + { + const string benchmarkClassName = "BenchmarkClass"; + + var genericTypeArgumentsAttributeUsages = Enumerable.Repeat("[GenericTypeArguments(typeof(int))]", genericTypeArgumentsAttributeUsageCount); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + {{string.Join("\n", genericTypeArgumentsAttributeUsages)}} + public class {|#0:{{benchmarkClassName}}|} + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Nongeneric_class_annotated_with_a_generictypearguments_attribute_inheriting_from_an_abstract_generic_class_should_trigger_diagnostic([CombinatorialMemberData(nameof(TypeParametersListLengthEnumerableLocal))] int typeParametersListLength, + [CombinatorialRange(1, 2)] int genericTypeArgumentsAttributeUsageCount, + [CombinatorialValues("", "[Benchmark]")] string benchmarkAttributeUsage) + { + const string benchmarkClassName = "BenchmarkClass"; + + var genericTypeArguments = string.Join(", ", GenericTypeArguments.Select(ta => $"typeof({ta})").Take(typeParametersListLength)); + var genericTypeArgumentsAttributeUsages = Enumerable.Repeat($"[GenericTypeArguments({genericTypeArguments})]", genericTypeArgumentsAttributeUsageCount); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + {{string.Join("\n", genericTypeArgumentsAttributeUsages)}} + public class {|#0:{{benchmarkClassName}}|} : BenchmarkClassBase<{{string.Join(", ", GenericTypeArguments.Take(typeParametersListLength))}}> + { + } + """; + + var benchmarkBaseClassDocument = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public abstract class BenchmarkClassBase<{{string.Join(", ", TypeParameters.Take(typeParametersListLength))}}> + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddSource(benchmarkBaseClassDocument); + + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + + public static IEnumerable TypeParametersListLengthEnumerableLocal => TypeParametersListLengthEnumerable; + + private static ReadOnlyCollection TypeParameters => TypeParametersTheoryData; + + private static ReadOnlyCollection GenericTypeArguments => GenericTypeArgumentsTheoryData; + } + + public class GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount : AnalyzerTestFixture + { + public GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount() : base(BenchmarkClassAnalyzer.GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCountRule) { } + + [Theory, CombinatorialData] + public async Task Generic_class_annotated_with_a_generictypearguments_attribute_having_matching_type_argument_count_should_not_trigger_diagnostic([CombinatorialMemberData(nameof(TypeParametersListLengthEnumerableLocal))] int typeParametersListLength, + [CombinatorialRange(1, 2)] int genericTypeArgumentsAttributeUsageCount, + [CombinatorialValues("", "[Benchmark]")] string benchmarkAttributeUsage) + { + var genericTypeArguments = string.Join(", ", GenericTypeArguments.Select(ta => $"typeof({ta})").Take(typeParametersListLength)); + var genericTypeArgumentsAttributeUsages = Enumerable.Repeat($"[GenericTypeArguments({genericTypeArguments})]", genericTypeArgumentsAttributeUsageCount); + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + {{string.Join("\n", genericTypeArgumentsAttributeUsages)}} + public class BenchmarkClass<{{string.Join(", ", TypeParameters.Take(typeParametersListLength))}}> + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Generic_class_annotated_with_a_generictypearguments_attribute_having_mismatching_type_argument_count_should_trigger_diagnostic([CombinatorialMemberData(nameof(TypeArgumentsData))] ValueTupleDouble typeArgumentsData, + [CombinatorialValues("", "[Benchmark]")] string benchmarkAttributeUsage) + { + const string benchmarkClassName = "BenchmarkClass"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + [GenericTypeArguments({|#0:{{typeArgumentsData.Value1}}|})] + [GenericTypeArguments(typeof(int))] + public class {{benchmarkClassName}} + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(1, "", benchmarkClassName, typeArgumentsData.Value2); + + await RunAsync(); + } + + public static IEnumerable> TypeArgumentsData => + [ + ("typeof(int), typeof(string)", 2), + ("typeof(int), typeof(string), typeof(bool)", 3) + ]; + + public static IEnumerable TypeParametersListLengthEnumerableLocal => TypeParametersListLengthEnumerable; + + private static ReadOnlyCollection TypeParameters => TypeParametersTheoryData; + + private static ReadOnlyCollection GenericTypeArguments => GenericTypeArgumentsTheoryData; + } + + public class MethodMustBePublic : AnalyzerTestFixture + { + public MethodMustBePublic() : base(BenchmarkClassAnalyzer.MethodMustBePublicRule) { } + + [Fact] + public async Task Public_method_annotated_with_benchmark_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + public void BenchmarkMethod() + { + + } + + public void NonBenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [ClassData(typeof(NonPublicClassMemberAccessModifiersTheoryData))] + public async Task Nonpublic_method_annotated_with_benchmark_attribute_should_trigger_diagnostic(string nonPublicClassAccessModifier) + { + const string benchmarkMethodName = "BenchmarkMethod"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + {{nonPublicClassAccessModifier}}void {|#0:{{benchmarkMethodName}}|}() + { + + } + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(benchmarkMethodName); + + await RunAsync(); + } + } + + public class MethodMustBeNonGeneric : AnalyzerTestFixture + { + public MethodMustBeNonGeneric() : base(BenchmarkClassAnalyzer.MethodMustBeNonGenericRule) { } + + [Fact] + public async Task Nongeneric_method_annotated_with_benchmark_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + public void NonGenericBenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task Generic_method_not_annotated_with_benchmark_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public void GenericMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Theory] + [MemberData(nameof(TypeParametersListLength))] + public async Task Nongeneric_method_annotated_with_benchmark_attribute_should_trigger_diagnostic(int typeParametersListLength) + { + const string benchmarkMethodName = "GenericBenchmarkMethod"; + + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + public void {{benchmarkMethodName}}{|#0:<{{string.Join(", ", TypeParameters.Take(typeParametersListLength))}}>|}() + { + + } + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(benchmarkMethodName); + + await RunAsync(); + } + + public static TheoryData TypeParametersListLength => TypeParametersListLengthTheoryData; + + private static ReadOnlyCollection TypeParameters => TypeParametersTheoryData; + } + + public class ClassMustBeNonStatic : AnalyzerTestFixture + { + public ClassMustBeNonStatic() : base(BenchmarkClassAnalyzer.ClassMustBeNonStaticRule) { } + + [Fact] + public async Task Instance_class_containing_at_least_one_method_annotated_with_benchmark_attribute_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + public void BenchmarkMethod() + { + + } + + public void NonBenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task Static_class_containing_at_least_one_method_annotated_with_benchmark_attribute_should_trigger_diagnostic() + { + const string benchmarkClassName = "BenchmarkClass"; + + const string testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public {|#0:static|} class {{benchmarkClassName}} + { + [Benchmark] + public static void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + AddDefaultExpectedDiagnostic(benchmarkClassName); + + await RunAsync(); + } + } + + public class OnlyOneMethodCanBeBaseline : AnalyzerTestFixture + { + public OnlyOneMethodCanBeBaseline() : base(BenchmarkClassAnalyzer.OnlyOneMethodCanBeBaselineRule) { } + + [Fact] + public async Task Class_with_only_one_benchmark_method_marked_as_baseline_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark(Baseline = true)] + public void BaselineBenchmarkMethod() + { + + } + + [Benchmark] + public void NonBaselineBenchmarkMethod1() + { + + } + + [Benchmark(Baseline = false)] + public void NonBaselineBenchmarkMethod2() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task Class_with_no_benchmark_methods_marked_as_baseline_should_not_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark] + public void NonBaselineBenchmarkMethod1() + { + + } + + [Benchmark] + public void NonBaselineBenchmarkMethod2() + { + + } + + [Benchmark(Baseline = false)] + public void NonBaselineBenchmarkMethod3() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + + [Fact] + public async Task Class_with_more_than_one_benchmark_method_marked_as_baseline_should_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Benchmark({|#0:Baseline = true|})] + [Benchmark] + public void BaselineBenchmarkMethod1() + { + + } + + [Benchmark] + public void NonBaselineBenchmarkMethod1() + { + + } + + [Benchmark(Baseline = false)] + public void NonBaselineBenchmarkMethod2() + { + + } + + [Benchmark({|#1:Baseline = true|})] + public void BaselineBenchmarkMethod2() + { + + } + + [Benchmark({|#2:Baseline = true|})] + [Benchmark({|#3:Baseline = true|})] + public void BaselineBenchmarkMethod3() + { + + } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + AddExpectedDiagnostic(0); + AddExpectedDiagnostic(1); + AddExpectedDiagnostic(2); + AddExpectedDiagnostic(3); + + await RunAsync(); + } + } + + public static TheoryData TypeParametersListLengthTheoryData => new(TypeParametersListLengthEnumerable); + + public static IEnumerable TypeParametersListLengthEnumerable => Enumerable.Range(1, TypeParametersTheoryData.Count); + + private static ReadOnlyCollection TypeParametersTheoryData => Enumerable.Range(1, 3) + .Select(i => $"TParameter{i}") + .ToList() + .AsReadOnly(); + private static ReadOnlyCollection GenericTypeArgumentsTheoryData => new List { "int", "string", "bool" }.AsReadOnly(); + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/BenchmarkDotNet.Analyzers.Tests.csproj b/tests/BenchmarkDotNet.Analyzers.Tests/BenchmarkDotNet.Analyzers.Tests.csproj new file mode 100644 index 0000000000..ee1e876743 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/BenchmarkDotNet.Analyzers.Tests.csproj @@ -0,0 +1,51 @@ + + + + net462;net6.0;net8.0 + enable + + true + true + true + true + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/BenchmarkDotNet.Analyzers.Tests.csproj.DotSettings b/tests/BenchmarkDotNet.Analyzers.Tests/BenchmarkDotNet.Analyzers.Tests.csproj.DotSettings new file mode 100644 index 0000000000..4ee741cd7e --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/BenchmarkDotNet.Analyzers.Tests.csproj.DotSettings @@ -0,0 +1,6 @@ + + True + True + True + True + True \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/AnalyzerTestFixture.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/AnalyzerTestFixture.cs new file mode 100644 index 0000000000..e92db1a01f --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/AnalyzerTestFixture.cs @@ -0,0 +1,230 @@ +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Testing; + using Microsoft.CodeAnalysis.Diagnostics; + using Microsoft.CodeAnalysis.Testing; + using Xunit; + + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + + public abstract class AnalyzerTestFixture + where TAnalyzer : DiagnosticAnalyzer, new() + { + private readonly CSharpAnalyzerTest _analyzerTest; + + private readonly DiagnosticDescriptor? _ruleUnderTest; + + private AnalyzerTestFixture(bool assertUniqueSupportedDiagnostics) + { + _analyzerTest = new InternalAnalyzerTest + { +#if NET8_0_OR_GREATER + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, +#elif NET6_0_OR_GREATER + ReferenceAssemblies = ReferenceAssemblies.Net.Net60, +#else + ReferenceAssemblies = ReferenceAssemblies.NetStandard.NetStandard20, +#endif + TestState = + { + AdditionalReferences = + { + "BenchmarkDotNet.dll", + "BenchmarkDotNet.Annotations.dll", +#if !NET6_0_OR_GREATER + "System.Memory.dll" +#endif + } + } + }; + + if (assertUniqueSupportedDiagnostics) + { + AssertUniqueSupportedDiagnostics(); + } + } + + protected AnalyzerTestFixture() : this(true) { } + + protected AnalyzerTestFixture(DiagnosticDescriptor diagnosticDescriptor) : this(false) + { + var analyzer = AssertUniqueSupportedDiagnostics(); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (diagnosticDescriptor == null) + { + Assert.Fail("Diagnostic under test cannot be null when using this constructor"); + } + + AssertDiagnosticUnderTestIsSupportedByAnalyzer(); + DisableAllSupportedDiagnosticsExceptDiagnosticUnderTest(); + + _ruleUnderTest = diagnosticDescriptor; + + return; + + void AssertDiagnosticUnderTestIsSupportedByAnalyzer() + { + if (!analyzer.SupportedDiagnostics.Any(dd => dd.Id == diagnosticDescriptor.Id)) + { + Assert.Fail($"Diagnostic descriptor with ID {diagnosticDescriptor.Id} is not supported by the analyzer {typeof(TAnalyzer).Name}"); + } + } + + void DisableAllSupportedDiagnosticsExceptDiagnosticUnderTest() + { + _analyzerTest.DisabledDiagnostics.Clear(); + _analyzerTest.DisabledDiagnostics.AddRange(analyzer.SupportedDiagnostics.Select(dd => dd.Id).Except([ + diagnosticDescriptor.Id + ])); + } + } + + private static TAnalyzer AssertUniqueSupportedDiagnostics() + { + var allSupportedDiagnostics = new Dictionary(); + + var analyzer = new TAnalyzer(); + foreach (var supportedDiagnostic in analyzer.SupportedDiagnostics) + { + if (allSupportedDiagnostics.ContainsKey(supportedDiagnostic.Id)) + { + allSupportedDiagnostics[supportedDiagnostic.Id]++; + } + else + { + allSupportedDiagnostics[supportedDiagnostic.Id] = 1; + } + } + + var duplicateSupportedDiagnostics = allSupportedDiagnostics.Where(kvp => kvp.Value > 1) + .OrderBy(kvp => kvp.Key) + .ToList(); + + if (duplicateSupportedDiagnostics.Count > 0) + { + Assert.Fail($"The analyzer {typeof(TAnalyzer).FullName} contains duplicate supported diagnostics:{Environment.NewLine}{Environment.NewLine}{string.Join(", ", duplicateSupportedDiagnostics.Select(kvp => $"❌ {kvp.Key} (x{kvp.Value})"))}{Environment.NewLine}"); + } + + return analyzer; + } + + protected string TestCode + { + set => _analyzerTest.TestCode = value; + } + + protected void AddSource(string filename, string content) => _analyzerTest.TestState.Sources.Add((filename, content)); + + protected void AddSource(string content) => _analyzerTest.TestState.Sources.Add(content); + + protected void AddDefaultExpectedDiagnostic() + { + AddExpectedDiagnostic(); + } + + protected void AddDefaultExpectedDiagnostic(params object[] arguments) + { + AddExpectedDiagnostic(arguments); + } + + protected void AddExpectedDiagnostic(int markupKey) + { + AddExpectedDiagnostic(null, markupKey); + } + + protected void AddExpectedDiagnostic(int markupKey, params object[] arguments) + { + AddExpectedDiagnostic(arguments, markupKey); + } + + private void AddExpectedDiagnostic(object[]? arguments = null, int markupKey = 0) + { + if (_ruleUnderTest == null) + { + throw new InvalidOperationException("Failed to add expected diagnostic: no diagnostic rule specified for this fixture"); + } + + var diagnosticResult = new DiagnosticResult(_ruleUnderTest).WithLocation(markupKey) + .WithMessageFormat(_ruleUnderTest.MessageFormat); + + if (arguments != null) + { + diagnosticResult = diagnosticResult.WithArguments(arguments); + } + + _analyzerTest.ExpectedDiagnostics.Add(diagnosticResult); + } + + protected void DisableCompilerDiagnostics() + { + _analyzerTest.CompilerDiagnostics = CompilerDiagnostics.None; + } + + protected Task RunAsync() + { + return _analyzerTest.RunAsync(CancellationToken.None); + } + + protected void ReferenceDummyAttribute() + { + _analyzerTest.TestState.Sources.Add(""" + using System; + + public class DummyAttribute : Attribute + { + + } + """); + } + + protected void ReferenceDummyEnum() + { + _analyzerTest.TestState.Sources.Add(""" + public enum DummyEnum + { + Value1, + Value2, + Value3 + } + """); + } + + protected void ReferenceDummyEnumWithFlagsAttribute() + { + _analyzerTest.TestState.Sources.Add(""" + using System; + + [Flags] + public enum DummyEnumWithFlagsAttribute + { + Value1, + Value2, + Value3 + } + """); + } + + protected void ReferenceConstants(string type, string value) + { + _analyzerTest.TestState.Sources.Add($$""" + using System; + + public static class Constants + { + public const {{type}} Value = {{value}}; + } + """); + } + + private sealed class InternalAnalyzerTest : CSharpAnalyzerTest + { + protected override string DefaultTestProjectName => "BenchmarksAssemblyUnderAnalysis"; + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Extensions/TheoryDataExtensions.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Extensions/TheoryDataExtensions.cs new file mode 100644 index 0000000000..e51a6fbc00 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Extensions/TheoryDataExtensions.cs @@ -0,0 +1,14 @@ +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + using Xunit; + + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + + public static class TheoryDataExtensions + { + public static ReadOnlyCollection AsReadOnly(this TheoryData theoryData) => (theoryData as IEnumerable).ToList() + .AsReadOnly(); + } +} \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Generators/CombinationsGenerator.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Generators/CombinationsGenerator.cs new file mode 100644 index 0000000000..55532f3ef5 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Generators/CombinationsGenerator.cs @@ -0,0 +1,66 @@ +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + + public static class CombinationsGenerator + { + public static IEnumerable GenerateCombinationsCounts(int length, int maxValue) + { + if (length <= 0) + { + yield break; + } + + var baseN = maxValue + 1; + var total = 1; + + for (var i = 0; i < length; i++) + { + total *= baseN; + } + + for (var i = 0; i < total; i++) + { + // ReSharper disable once StackAllocInsideLoop + Span currentCombination = stackalloc int[length]; + + var temp = i; + for (var j = length - 1; j >= 0; j--) + { + currentCombination[j] = temp % baseN; + temp /= baseN; + } + + // Copy from Span (stack) to heap-allocated array + var result = new int[length]; + currentCombination.CopyTo(result); + + yield return result; + } + } + + public static IEnumerable CombineArguments(params IEnumerable[] argumentSets) + { + if (argumentSets.Length == 0) + { + yield break; + } + + IEnumerable combinations = [[]]; + + foreach (var argumentValues in argumentSets) + { + combinations = combinations.SelectMany(_ => argumentValues.Cast(), (c, v) => c.Concat([v]) + .ToArray()); + } + + foreach (var combination in combinations) + { + yield return combination; + } + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Serializable/ValueTupleDouble.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Serializable/ValueTupleDouble.cs new file mode 100644 index 0000000000..c7b6309cea --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Serializable/ValueTupleDouble.cs @@ -0,0 +1,27 @@ +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + using Xunit.Abstractions; + + public class ValueTupleDouble : IXunitSerializable + { + public T1? Value1 { get; set; } + + public T2? Value2 { get; set; } + + public void Deserialize(IXunitSerializationInfo info) + { + Value1 = info.GetValue(nameof(Value1)); + Value2 = info.GetValue(nameof(Value2)); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(Value1), Value1); + info.AddValue(nameof(Value2), Value2); + } + + public static implicit operator ValueTupleDouble((T1, T2) valueTupleDouble) => new() { Value1 = valueTupleDouble.Item1, Value2 = valueTupleDouble.Item2 }; + + public override string ToString() => Value1 == null || Value2 == null ? "" : $"{Value1} · {Value2}"; + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Serializable/ValueTupleTriple.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Serializable/ValueTupleTriple.cs new file mode 100644 index 0000000000..ae3cd9614b --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/Serializable/ValueTupleTriple.cs @@ -0,0 +1,31 @@ +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + using Xunit.Abstractions; + + public class ValueTupleTriple : IXunitSerializable + { + public T1? Value1 { get; set; } + + public T2? Value2 { get; set; } + + public T3? Value3 { get; set; } + + public void Deserialize(IXunitSerializationInfo info) + { + Value1 = info.GetValue(nameof(Value1)); + Value2 = info.GetValue(nameof(Value2)); + Value3 = info.GetValue(nameof(Value3)); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(Value1), Value1); + info.AddValue(nameof(Value2), Value2); + info.AddValue(nameof(Value3), Value3); + } + + public static implicit operator ValueTupleTriple((T1, T2, T3) valueTupleTriple) => new() { Value1 = valueTupleTriple.Item1, Value2 = valueTupleTriple.Item2, Value3 = valueTupleTriple.Item3 }; + + public override string ToString() => Value1 == null || Value2 == null || Value3 == null ? "" : $"{Value1} · {Value2} · {Value3}"; + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/FieldOrPropertyDeclarationsTheoryData.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/FieldOrPropertyDeclarationsTheoryData.cs new file mode 100644 index 0000000000..9ddaacf203 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/FieldOrPropertyDeclarationsTheoryData.cs @@ -0,0 +1,19 @@ +using Xunit; + +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + internal sealed class FieldOrPropertyDeclarationsTheoryData : TheoryData + { + public FieldOrPropertyDeclarationsTheoryData() + { + AddRange( +#if NET5_0_OR_GREATER + "Property { get; init; }", +#else + "Property { get; set; }", +#endif + "_field;" + ); + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicClassAccessModifiersTheoryData.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicClassAccessModifiersTheoryData.cs new file mode 100644 index 0000000000..76f270f8af --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicClassAccessModifiersTheoryData.cs @@ -0,0 +1,20 @@ +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + using Xunit; + + internal sealed class NonPublicClassAccessModifiersTheoryData : TheoryData + { + public NonPublicClassAccessModifiersTheoryData() + { + AddRange( + "protected internal ", + "protected ", + "internal ", + "private protected ", + "private ", + "file ", + "" + ); + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicClassMemberAccessModifiersTheoryData.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicClassMemberAccessModifiersTheoryData.cs new file mode 100644 index 0000000000..6b3599dc4a --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicClassMemberAccessModifiersTheoryData.cs @@ -0,0 +1,19 @@ +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + using Xunit; + + internal sealed class NonPublicClassMemberAccessModifiersTheoryData : TheoryData + { + public NonPublicClassMemberAccessModifiersTheoryData() + { + AddRange( + "protected internal ", + "protected ", + "internal ", + "private protected ", + "private ", + "" + ); + } + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicPropertySetterAccessModifiersTheoryData.cs b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicPropertySetterAccessModifiersTheoryData.cs new file mode 100644 index 0000000000..7138a02507 --- /dev/null +++ b/tests/BenchmarkDotNet.Analyzers.Tests/Fixtures/TheoryData/NonPublicPropertySetterAccessModifiersTheoryData.cs @@ -0,0 +1,18 @@ +namespace BenchmarkDotNet.Analyzers.Tests.Fixtures +{ + using Xunit; + + internal sealed class NonPublicPropertySetterAccessModifiersTheoryData : TheoryData + { + public NonPublicPropertySetterAccessModifiersTheoryData() + { + AddRange( + "protected internal", + "protected", + "internal", + "private protected", + "private" + ); + } + } +}