From 55760a20a1012028d195bb06f497f61c8dfd4177 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 15 Nov 2025 21:52:01 -0500 Subject: [PATCH 1/6] Multi-target analyzers --- build/BenchmarkDotNet.Build/BuildContext.cs | 2 ++ build/BenchmarkDotNet.Build/Program.cs | 20 +++++++++++ .../Runners/BuildRunner.cs | 18 ++++++++++ build/common.props | 1 - .../AnalyzerHelper.cs | 8 ++--- .../Attributes/ArgumentsAttributeAnalyzer.cs | 35 ++++++++++--------- .../GeneralArgumentAttributesAnalyzer.cs | 8 ++--- .../GeneralParameterAttributesAnalyzer.cs | 26 +++++++------- .../ParamsAllValuesAttributeAnalyzer.cs | 12 +++---- .../Attributes/ParamsAttributeAnalyzer.cs | 30 ++++++++-------- .../BenchmarkDotNet.Analyzers.csproj | 23 +++++++++++- .../BenchmarkRunner/RunAnalyzer.cs | 12 +++---- .../General/BenchmarkClassAnalyzer.cs | 23 ++++++------ .../BenchmarkDotNet.Annotations.csproj | 3 +- 14 files changed, 144 insertions(+), 77 deletions(-) diff --git a/build/BenchmarkDotNet.Build/BuildContext.cs b/build/BenchmarkDotNet.Build/BuildContext.cs index 3e3c42c27d..f3aaee4bde 100644 --- a/build/BenchmarkDotNet.Build/BuildContext.cs +++ b/build/BenchmarkDotNet.Build/BuildContext.cs @@ -30,6 +30,7 @@ public class BuildContext : FrostingContext public DirectoryPath ArtifactsDirectory { get; } public FilePath SolutionFile { get; } + public FilePath AnalyzersProjectFile { get; } public FilePath TemplatesTestsProjectFile { get; } public FilePathCollection AllPackableSrcProjects { get; } public FilePath VersionsFile { get; } @@ -64,6 +65,7 @@ public BuildContext(ICakeContext context) context.Tools.RegisterFile(toolFilePath); SolutionFile = RootDirectory.CombineWithFilePath("BenchmarkDotNet.sln"); + AnalyzersProjectFile = RootDirectory.Combine("src").Combine("BenchmarkDotNet.Analyzers").CombineWithFilePath("BenchmarkDotNet.Analyzers.csproj"); TemplatesTestsProjectFile = RootDirectory.Combine("templates") .CombineWithFilePath("BenchmarkDotNet.Templates.csproj"); diff --git a/build/BenchmarkDotNet.Build/Program.cs b/build/BenchmarkDotNet.Build/Program.cs index 825d1e1554..f18df86a9a 100644 --- a/build/BenchmarkDotNet.Build/Program.cs +++ b/build/BenchmarkDotNet.Build/Program.cs @@ -156,9 +156,29 @@ public class AllTestsTask : FrostingTask, IHelpProvider public HelpInfo GetHelp() => new(); } +[TaskName(Name)] +[TaskDescription("Build BenchmarkDotNet.Analyzers")] +public class BuildAnalyzersTask : FrostingTask, IHelpProvider +{ + private const string Name = "build-analyzers"; + public override void Run(BuildContext context) => context.BuildRunner.BuildAnalyzers(); + + public HelpInfo GetHelp() + { + return new HelpInfo + { + Examples = + [ + new Example(Name) + ] + }; + } +} + [TaskName(Name)] [TaskDescription("Pack Nupkg packages")] [IsDependentOn(typeof(BuildTask))] +[IsDependentOn(typeof(BuildAnalyzersTask))] public class PackTask : FrostingTask, IHelpProvider { private const string Name = "pack"; diff --git a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs index a38ce7e79d..4998f2e0db 100644 --- a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs +++ b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs @@ -64,6 +64,24 @@ public void BuildProjectSilent(FilePath projectFile) }); } + public void BuildAnalyzers() + { + context.Information("BuildSystemProvider: " + context.BuildSystem().Provider); + string[] mccVersions = ["2.8", "3.8", "4.8", "5.0"]; + foreach (string version in mccVersions) + { + context.DotNetBuild(context.AnalyzersProjectFile.FullPath, new DotNetBuildSettings + { + NoRestore = true, + DiagnosticOutput = true, + MSBuildSettings = context.MsBuildSettingsBuild, + Configuration = context.BuildConfiguration, + Verbosity = context.BuildVerbosity, + ArgumentCustomization = args => args.Append($"-p:MccVersion={version}") + }); + } + } + public void Pack() { context.CleanDirectory(context.ArtifactsDirectory); diff --git a/build/common.props b/build/common.props index 5cff882358..869a2abd67 100644 --- a/build/common.props +++ b/build/common.props @@ -25,7 +25,6 @@ annotations true - CS9057 diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs index ab65c0219b..2be51c6845 100644 --- a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs @@ -38,7 +38,7 @@ public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTyp continue; } - if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol)) { return true; } @@ -58,7 +58,7 @@ public static bool AttributeListContainsAttribute(INamedTypeSymbol? attributeTyp return false; } - return attributeList.Any(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default)); + return attributeList.Any(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(attributeTypeSymbol)); } public static ImmutableArray GetAttributes(string attributeName, Compilation compilation, SyntaxList attributeLists, SemanticModel semanticModel) @@ -83,7 +83,7 @@ public static ImmutableArray GetAttributes(INamedTypeSymbol? at continue; } - if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol)) { attributesBuilder.Add(attributeSyntax); } @@ -115,7 +115,7 @@ public static int GetAttributeUsageCount(INamedTypeSymbol? attributeTypeSymbol, continue; } - if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol)) { attributeUsageCount++; } diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs index e309a579cf..12fb5251c1 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs @@ -37,12 +37,12 @@ public class ArgumentsAttributeAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description))); - public override ImmutableArray SupportedDiagnostics => - [ + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] + { RequiresBenchmarkAttributeRule, MustHaveMatchingValueCountRule, - MustHaveMatchingValueTypeRule - ]; + MustHaveMatchingValueTypeRule, + }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) { @@ -145,8 +145,8 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) } } +#if CODE_ANALYSIS_4_8 // Collection expression - else if (attributeArgumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax) { if (methodDeclarationSyntax.ParameterList.Parameters.Count != collectionExpressionSyntax.Elements.Count) @@ -163,6 +163,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(i => collectionExpressionSyntax.Elements[i] is ExpressionElementSyntax expressionElementSyntax ? expressionElementSyntax.Expression : null); } +#endif // Array creation expression else @@ -193,7 +194,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) ReportMustHaveMatchingValueCountDiagnostic( arrayCreationExpressionSyntax.Initializer.Expressions.Count == 0 ? arrayCreationExpressionSyntax.Initializer.GetLocation() - : Location.Create(context.FilterTree, arrayCreationExpressionSyntax.Initializer.Expressions.Span), + : Location.Create(context.Node.SyntaxTree, arrayCreationExpressionSyntax.Initializer.Expressions.Span), arrayCreationExpressionSyntax.Initializer.Expressions.Count ); @@ -216,7 +217,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) { ReportMustHaveMatchingValueCountDiagnostic( Location.Create( - context.FilterTree, + context.Node.SyntaxTree, TextSpan.FromBounds(argumentsAttributeSyntax.ArgumentList.Arguments.Span.Start, argumentsAttributeSyntax.ArgumentList.Arguments[firstNamedArgumentIndex.Value - 1].Span.End) ), firstNamedArgumentIndex.Value @@ -233,7 +234,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) if (methodDeclarationSyntax.ParameterList.Parameters.Count != argumentsAttributeSyntax.ArgumentList.Arguments.Count) { ReportMustHaveMatchingValueCountDiagnostic( - Location.Create(context.FilterTree, argumentsAttributeSyntax.ArgumentList.Arguments.Span), + Location.Create(context.Node.SyntaxTree, argumentsAttributeSyntax.ArgumentList.Arguments.Span), argumentsAttributeSyntax.ArgumentList.Arguments.Count ); @@ -295,9 +296,9 @@ or TypeKind.Enum var typeTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Type"); - if (methodParameterTypeSymbol.Equals(typeTypeSymbol, SymbolEqualityComparer.Default)) + if (methodParameterTypeSymbol.Equals(typeTypeSymbol)) { - if (!actualValueTypeSymbol.Equals(typeTypeSymbol, SymbolEqualityComparer.Default)) + if (!actualValueTypeSymbol.Equals(typeTypeSymbol)) { ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( valueExpressionSyntax.GetLocation(), @@ -319,9 +320,9 @@ or TypeKind.Enum if (actualValueTypeSymbol is IArrayTypeSymbol actualValueArrayTypeSymbol) { - if (methodParameterTypeSymbol is IArrayTypeSymbol expectedValueArrayTypeSymbol && expectedValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol, SymbolEqualityComparer.Default)) + if (methodParameterTypeSymbol is IArrayTypeSymbol expectedValueArrayTypeSymbol && expectedValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol)) { - if (!actualValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol, SymbolEqualityComparer.Default)) + if (!actualValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol)) { ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( valueExpressionSyntax.GetLocation(), @@ -341,13 +342,15 @@ or TypeKind.Enum valueTypeContainingNamespace = actualValueArrayTypeSymbol.ElementType.ContainingNamespace.ToString(); } } - else if (actualValueArrayTypeSymbol.ElementType.TypeKind is TypeKind.Struct) + else if (actualValueArrayTypeSymbol.ElementType.TypeKind == TypeKind.Struct) { - if (actualValueArrayTypeSymbol.ElementType.NullableAnnotation == NullableAnnotation.Annotated) + // Nullable + if (actualValueArrayTypeSymbol.ElementType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) { continue; } } + else if (actualValueArrayTypeSymbol.ElementType.TypeKind is not TypeKind.Class) { continue; @@ -355,7 +358,7 @@ or TypeKind.Enum } if (!AnalyzerHelper.IsAssignableToLocal(context.Compilation, - (context.FilterTree.Options as CSharpParseOptions)!.LanguageVersion, + (context.Node.SyntaxTree.Options as CSharpParseOptions)!.LanguageVersion, valueTypeContainingNamespace, methodParameterTypeSymbol, valueExpressionString, @@ -373,7 +376,7 @@ or TypeKind.Enum else if (constantValue is { HasValue: true, Value: null }) { if (!AnalyzerHelper.IsAssignableToField(context.Compilation, - (context.FilterTree.Options as CSharpParseOptions)!.LanguageVersion, + (context.Node.SyntaxTree.Options as CSharpParseOptions)!.LanguageVersion, null, methodParameterTypeSymbol, valueExpressionString, diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralArgumentAttributesAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralArgumentAttributesAnalyzer.cs index 7b386731ec..06a0d1521d 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralArgumentAttributesAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralArgumentAttributesAnalyzer.cs @@ -18,10 +18,10 @@ public class GeneralArgumentAttributesAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: BenchmarkDotNetAnalyzerResources.Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_Description); - public override ImmutableArray SupportedDiagnostics => - [ + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] + { MethodWithoutAttributeMustHaveNoParametersRule, - ]; + }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) { @@ -66,7 +66,7 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( MethodWithoutAttributeMustHaveNoParametersRule, - Location.Create(context.FilterTree, methodDeclarationSyntax.ParameterList.Parameters.Span), methodDeclarationSyntax.Identifier.ToString()) + Location.Create(context.Node.SyntaxTree, methodDeclarationSyntax.ParameterList.Parameters.Span), methodDeclarationSyntax.Identifier.ToString()) ); } } diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs index afaf232613..7a014bdbab 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs @@ -82,8 +82,8 @@ public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description))); - public override ImmutableArray SupportedDiagnostics => - [ + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] + { MutuallyExclusiveOnFieldRule, MutuallyExclusiveOnPropertyRule, FieldMustBePublic, @@ -91,8 +91,8 @@ public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer NotValidOnReadonlyFieldRule, NotValidOnConstantFieldRule, PropertyCannotBeInitOnlyRule, - PropertyMustHavePublicSetterRule - ]; + PropertyMustHavePublicSetterRule, + }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) { @@ -128,9 +128,9 @@ private static void Analyze(SyntaxNodeAnalysisContext context) if (attributeSyntaxTypeSymbol == null || attributeSyntaxTypeSymbol.TypeKind == TypeKind.Error || - (!attributeSyntaxTypeSymbol.Equals(paramsAttributeTypeSymbol, SymbolEqualityComparer.Default) - && !attributeSyntaxTypeSymbol.Equals(paramsSourceAttributeTypeSymbol, SymbolEqualityComparer.Default) - && !attributeSyntaxTypeSymbol.Equals(paramsAllValuesAttributeTypeSymbol, SymbolEqualityComparer.Default))) + (!attributeSyntaxTypeSymbol.Equals(paramsAttributeTypeSymbol) + && !attributeSyntaxTypeSymbol.Equals(paramsSourceAttributeTypeSymbol) + && !attributeSyntaxTypeSymbol.Equals(paramsAllValuesAttributeTypeSymbol))) { return; } @@ -174,8 +174,10 @@ private static void Analyze(SyntaxNodeAnalysisContext context) fieldOrPropertyIsPublic = propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword); fieldOrPropertyIdentifier = propertyDeclarationSyntax.Identifier.ToString(); +#if CODE_ANALYSIS_3_8 var propertyInitAccessorIndex = propertyDeclarationSyntax.AccessorList?.Accessors.IndexOf(SyntaxKind.InitAccessorDeclaration); propertyInitAccessorKeywordLocation = propertyInitAccessorIndex >= 0 ? propertyDeclarationSyntax.AccessorList.Accessors[propertyInitAccessorIndex.Value].Keyword.GetLocation() : null; +#endif var propertySetAccessorIndex = propertyDeclarationSyntax.AccessorList?.Accessors.IndexOf(SyntaxKind.SetAccessorDeclaration); propertyIsMissingAssignableSetter = !propertySetAccessorIndex.HasValue || propertySetAccessorIndex.Value < 0 || propertyDeclarationSyntax.AccessorList.Accessors[propertySetAccessorIndex.Value].Modifiers.Any(); @@ -224,14 +226,14 @@ private static void AnalyzeFieldOrPropertySymbol( DiagnosticDescriptor fieldOrPropertyMustBePublicDiagnosticRule, AttributeSyntax attributeSyntax) { - ImmutableArray applicableParameterAttributeTypeSymbols = - [ + ImmutableArray applicableParameterAttributeTypeSymbols = new INamedTypeSymbol[] + { paramsAttributeTypeSymbol, paramsSourceAttributeTypeSymbol, paramsAllValuesAttributeTypeSymbol - ]; + }.ToImmutableArray(); - var parameterAttributeTypeSymbols = new HashSet(SymbolEqualityComparer.Default); + var parameterAttributeTypeSymbols = new HashSet(); foreach (var declaredAttributeSyntax in declaredAttributes) { @@ -240,7 +242,7 @@ private static void AnalyzeFieldOrPropertySymbol( { foreach (var applicableParameterAttributeTypeSymbol in applicableParameterAttributeTypeSymbols) { - if (declaredAttributeTypeSymbol.Equals(applicableParameterAttributeTypeSymbol, SymbolEqualityComparer.Default)) + if (declaredAttributeTypeSymbol.Equals(applicableParameterAttributeTypeSymbol)) { if (!parameterAttributeTypeSymbols.Add(applicableParameterAttributeTypeSymbol)) { diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs index 4abf9fffe0..3f637f5613 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs @@ -27,11 +27,11 @@ public class ParamsAllValuesAttributeAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Error, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => - [ + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] + { NotAllowedOnFlagsEnumPropertyOrFieldTypeRule, - PropertyOrFieldTypeMustBeEnumOrBoolRule - ]; + PropertyOrFieldTypeMustBeEnumOrBoolRule, + }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) { @@ -60,7 +60,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var paramsAllValuesAttributeTypeSymbol = GetParamsAllValuesAttributeTypeSymbol(context.Compilation); var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; - if (attributeSyntaxTypeSymbol == null || !attributeSyntaxTypeSymbol.Equals(paramsAllValuesAttributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attributeSyntaxTypeSymbol == null || !attributeSyntaxTypeSymbol.Equals(paramsAllValuesAttributeTypeSymbol)) { return; } @@ -111,7 +111,7 @@ private static void AnalyzeFieldOrPropertyTypeSyntax(SyntaxNodeAnalysisContext c return; } - if (fieldOrPropertyTypeSymbol.GetAttributes().Any(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(flagsAttributeTypeSymbol, SymbolEqualityComparer.Default))) + if (fieldOrPropertyTypeSymbol.GetAttributes().Any(ad => ad.AttributeClass != null && ad.AttributeClass.Equals(flagsAttributeTypeSymbol))) { context.ReportDiagnostic(Diagnostic.Create(NotAllowedOnFlagsEnumPropertyOrFieldTypeRule, fieldOrPropertyTypeSyntax.GetLocation(), fieldOrPropertyTypeSymbol.ToString())); } diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs index c14ae40cfe..d152fab6b0 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs @@ -35,12 +35,12 @@ public class ParamsAttributeAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Info, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => - [ + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] + { MustHaveValuesRule, MustHaveMatchingValueTypeRule, - UnnecessarySingleValuePassedToAttributeRule - ]; + UnnecessarySingleValuePassedToAttributeRule, + }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) { @@ -69,7 +69,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var paramsAttributeTypeSymbol = GetParamsAttributeTypeSymbol(context.Compilation); var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; - if (attributeSyntaxTypeSymbol == null || !attributeSyntaxTypeSymbol.Equals(paramsAttributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attributeSyntaxTypeSymbol == null || !attributeSyntaxTypeSymbol.Equals(paramsAttributeTypeSymbol)) { return; } @@ -117,7 +117,7 @@ private static void AnalyzeFieldOrPropertyTypeSyntax(SyntaxNodeAnalysisContext c if (attributeSyntax.ArgumentList.Arguments.All(aas => aas.NameEquals != null)) { - context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, Location.Create(context.FilterTree, attributeSyntax.ArgumentList.Arguments.Span))); + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, Location.Create(context.Node.SyntaxTree, attributeSyntax.ArgumentList.Arguments.Span))); return; } @@ -138,8 +138,8 @@ private static void AnalyzeFieldOrPropertyTypeSyntax(SyntaxNodeAnalysisContext c return; } +#if CODE_ANALYSIS_4_8 // Collection expression - if (attributeArgumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax) { if (!collectionExpressionSyntax.Elements.Any()) @@ -163,6 +163,7 @@ private static void AnalyzeFieldOrPropertyTypeSyntax(SyntaxNodeAnalysisContext c return; } +#endif // Array creation expression @@ -245,9 +246,9 @@ or TypeKind.Enum var typeTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Type"); - if (expectedValueTypeSymbol.Equals(typeTypeSymbol, SymbolEqualityComparer.Default)) + if (expectedValueTypeSymbol.Equals(typeTypeSymbol)) { - if (!actualValueTypeSymbol.Equals(typeTypeSymbol, SymbolEqualityComparer.Default)) + if (!actualValueTypeSymbol.Equals(typeTypeSymbol)) { ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( valueExpressionSyntax.GetLocation(), @@ -269,9 +270,9 @@ or TypeKind.Enum if (actualValueTypeSymbol is IArrayTypeSymbol actualValueArrayTypeSymbol) { - if (expectedValueTypeSymbol is IArrayTypeSymbol expectedValueArrayTypeSymbol && expectedValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol, SymbolEqualityComparer.Default)) + if (expectedValueTypeSymbol is IArrayTypeSymbol expectedValueArrayTypeSymbol && expectedValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol)) { - if (!actualValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol, SymbolEqualityComparer.Default)) + if (!actualValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol)) { ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( valueExpressionSyntax.GetLocation(), @@ -293,7 +294,8 @@ or TypeKind.Enum } else if (actualValueArrayTypeSymbol.ElementType.TypeKind is TypeKind.Struct) { - if (actualValueArrayTypeSymbol.ElementType.NullableAnnotation == NullableAnnotation.Annotated) + // Nullable + if (actualValueArrayTypeSymbol.ElementType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) { return; } @@ -305,7 +307,7 @@ or TypeKind.Enum } if (!AnalyzerHelper.IsAssignableToField(context.Compilation, - (context.FilterTree.Options as CSharpParseOptions)!.LanguageVersion, + (context.Node.SyntaxTree.Options as CSharpParseOptions)!.LanguageVersion, valueTypeContainingNamespace, expectedValueTypeSymbol, valueExpressionString, @@ -323,7 +325,7 @@ or TypeKind.Enum else if (constantValue is { HasValue: true, Value: null }) { if (!AnalyzerHelper.IsAssignableToField(context.Compilation, - (context.FilterTree.Options as CSharpParseOptions)!.LanguageVersion, + (context.Node.SyntaxTree.Options as CSharpParseOptions)!.LanguageVersion, null, expectedValueTypeSymbol, valueExpressionString, diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj index 737f3ceb23..9162a8ae7d 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj @@ -7,9 +7,30 @@ BenchmarkDotNet.Analyzers true $(NoWarn);CS1591 + + + 4.8 + bin\$(Configuration)\roslyn$(MccVersion)\cs + false + $(DefineConstants);CODE_ANALYSIS_3_8 + $(DefineConstants);CODE_ANALYSIS_4_8 + $(DefineConstants);CODE_ANALYSIS_5_0 + + + $(NoWarn);RS1024;RS2007 + + $(NoWarn);RS2002 - + + + + + + + + + diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs index 104ff758af..efcb46a2a2 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs @@ -54,14 +54,14 @@ public class RunAnalyzer : DiagnosticAnalyzer description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Description))); - public override ImmutableArray SupportedDiagnostics => - [ + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] + { TypeArgumentClassMissingBenchmarkMethodsRule, TypeArgumentClassMustBePublicRule, TypeArgumentClassMustBeUnsealedRule, TypeArgumentClassMustBeNonAbstractRule, GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttributeRule, - ]; + }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) { @@ -108,7 +108,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var classMemberAccessTypeSymbol = context.SemanticModel.GetTypeInfo(identifierNameSyntax).Type; if (classMemberAccessTypeSymbol is null || classMemberAccessTypeSymbol.TypeKind == TypeKind.Error - || !classMemberAccessTypeSymbol.Equals(benchmarkRunnerTypeSymbol, SymbolEqualityComparer.Default)) + || !classMemberAccessTypeSymbol.Equals(benchmarkRunnerTypeSymbol)) { return; } @@ -128,7 +128,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) return; } - diagnosticLocation = Location.Create(context.FilterTree, genericMethod.TypeArgumentList.Arguments.Span); + diagnosticLocation = Location.Create(context.Node.SyntaxTree, genericMethod.TypeArgumentList.Arguments.Span); benchmarkClassTypeSymbol = context.SemanticModel.GetTypeInfo(genericMethod.TypeArgumentList.Arguments[0]).Type as INamedTypeSymbol; } else @@ -202,7 +202,7 @@ bool HasBenchmarkAttribute() { if (attributeData.AttributeClass != null) { - if (attributeData.AttributeClass.Equals(benchmarkAttributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attributeData.AttributeClass.Equals(benchmarkAttributeTypeSymbol)) { return true; } diff --git a/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs index 576664fc1d..a2b414da43 100644 --- a/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs @@ -90,8 +90,8 @@ public class BenchmarkClassAnalyzer : DiagnosticAnalyzer DiagnosticSeverity.Warning, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => - [ + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] + { ClassWithGenericTypeArgumentsAttributeMustBeNonAbstractRule, ClassWithGenericTypeArgumentsAttributeMustBeGenericRule, GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCountRule, @@ -100,8 +100,8 @@ public class BenchmarkClassAnalyzer : DiagnosticAnalyzer ClassMustBeNonStaticRule, SingleNullArgumentToBenchmarkCategoryAttributeNotAllowedRule, OnlyOneMethodCanBeBaselineRule, - OnlyOneMethodCanBeBaselinePerCategoryRule - ]; + OnlyOneMethodCanBeBaselinePerCategoryRule, + }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) { @@ -163,7 +163,7 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) if (genericTypeArgumentsAttribute.ArgumentList.Arguments.Count != classDeclarationSyntax.TypeParameterList.Parameters.Count) { context.ReportDiagnostic(Diagnostic.Create(GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCountRule, - Location.Create(context.FilterTree, genericTypeArgumentsAttribute.ArgumentList.Arguments.Span), + Location.Create(context.Node.SyntaxTree, genericTypeArgumentsAttribute.ArgumentList.Arguments.Span), classDeclarationSyntax.TypeParameterList.Parameters.Count, classDeclarationSyntax.TypeParameterList.Parameters.Count == 1 ? "" : "s", classDeclarationSyntax.Identifier.ToString(), @@ -208,11 +208,11 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) continue; } - if (attributeSyntaxTypeSymbol.Equals(benchmarkAttributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attributeSyntaxTypeSymbol.Equals(benchmarkAttributeTypeSymbol)) { benchmarkAttributeUsages.Add(attributeSyntax); } - else if (attributeSyntaxTypeSymbol.Equals(benchmarkCategoryAttributeTypeSymbol, SymbolEqualityComparer.Default)) + else if (attributeSyntaxTypeSymbol.Equals(benchmarkCategoryAttributeTypeSymbol)) { if (attributeSyntax.ArgumentList is { Arguments.Count: 1 }) { @@ -220,8 +220,8 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) Optional constantValue; +#if CODE_ANALYSIS_4_8 // Collection expression - if (attributeSyntax.ArgumentList.Arguments[0].Expression is CollectionExpressionSyntax collectionExpressionSyntax) { foreach (var collectionElementSyntax in collectionExpressionSyntax.Elements) @@ -251,6 +251,7 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) continue; } +#endif // Array creation expression @@ -452,11 +453,11 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) foreach (var attribute in methodAttributes) { - if (attribute.AttributeClass.Equals(benchmarkAttributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attribute.AttributeClass.Equals(benchmarkAttributeTypeSymbol)) { benchmarkAttributeUsages.Add(attribute); } - else if (attribute.AttributeClass.Equals(benchmarkCategoryAttributeTypeSymbol, SymbolEqualityComparer.Default)) + else if (attribute.AttributeClass.Equals(benchmarkCategoryAttributeTypeSymbol)) { foreach (var benchmarkCategoriesArray in attribute.ConstructorArguments) { @@ -549,7 +550,7 @@ private static void AnalyzeAttributeSyntax(SyntaxNodeAnalysisContext context) var benchmarkCategoryAttributeTypeSymbol = GetBenchmarkCategoryAttributeTypeSymbol(context.Compilation); var attributeTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; - if (attributeTypeSymbol != null && attributeTypeSymbol.Equals(benchmarkCategoryAttributeTypeSymbol, SymbolEqualityComparer.Default)) + if (attributeTypeSymbol != null && attributeTypeSymbol.Equals(benchmarkCategoryAttributeTypeSymbol)) { if (attributeSyntax.ArgumentList is { Arguments.Count: 1 }) { diff --git a/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj b/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj index 1048cd2813..a6cadd1f88 100644 --- a/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj +++ b/src/BenchmarkDotNet.Annotations/BenchmarkDotNet.Annotations.csproj @@ -15,7 +15,6 @@ - - + \ No newline at end of file From 5edf81c170f4696e59aa95925e9798012a46a21c Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sat, 15 Nov 2025 21:55:34 -0500 Subject: [PATCH 2/6] Fix BDN1502 in net462. --- tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs b/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs index c168071169..76edfe5d4e 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs @@ -428,8 +428,9 @@ public void AcceptsSpanFromArgumentsSource(Span span) } } - [TheoryEnvSpecific("The implicit cast operator is available only in .NET Core 2.1+ (See https://github.com/dotnet/corefx/issues/30121 for more)", - EnvRequirement.DotNetCoreOnly)] + // The string -> ReadOnlySpan implicit cast operator is available only in .NET Core 2.1+ (https://github.com/dotnet/corefx/issues/30121) +#if NETCOREAPP2_1_OR_GREATER + [Theory] [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] public void StringCanBePassedToBenchmarkAsReadOnlySpan(IToolchain toolchain) => CanExecute(toolchain); @@ -448,8 +449,7 @@ public void AcceptsReadOnlySpan(ReadOnlySpan notString) } } - [TheoryEnvSpecific("The implicit cast operator is available only in .NET Core 2.1+ (See https://github.com/dotnet/corefx/issues/30121 for more)", - EnvRequirement.DotNetCoreOnly)] + [Theory] [MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] public void StringFromArgumentsSourceCanBePassedToBenchmarkAsReadOnlySpan(IToolchain toolchain) => CanExecute(toolchain); @@ -472,6 +472,7 @@ public void AcceptsReadOnlySpanFromArgumentsSource(ReadOnlySpan notString) throw new ArgumentException("Invalid value"); } } +#endif [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] public void AnArrayOfStringsCanBeUsedAsArgument(IToolchain toolchain) => From 7724e90f66dc7a1a1bc63789a3886800840772c2 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sun, 16 Nov 2025 07:26:34 -0500 Subject: [PATCH 3/6] Refactored ArgumentsAttributeAnalyzer to use semantic model before syntax model for cleaner testing of assignability. Added BDN1503. WIP need to do the same for Params. --- .../AnalyzerHelper.cs | 115 +++-- .../AnalyzerReleases.Unshipped.md | 6 + .../Attributes/ArgumentsAttributeAnalyzer.cs | 394 ++++-------------- .../Attributes/ParamsAttributeAnalyzer.cs | 95 +---- ...nchmarkDotNetAnalyzerResources.Designer.cs | 22 +- .../BenchmarkDotNetAnalyzerResources.resx | 10 +- .../DiagnosticIds.cs | 1 + .../ArgumentsAttributeAnalyzerTests.cs | 158 ++----- 8 files changed, 250 insertions(+), 551 deletions(-) diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs index 2be51c6845..c2d1274069 100644 --- a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Globalization; using System.Linq; @@ -145,58 +146,52 @@ public static string NormalizeTypeName(INamedTypeSymbol namedTypeSymbol) return typeName; } - public static bool IsAssignableToField(Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) + public static bool IsAssignableToField(Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) { const string codeTemplate1 = """ - {0} - file static class Internal {{ - static readonly {1} x = {2}; + static readonly {0} x = {1}; }} """; const string codeTemplate2 = """ - {0} - file static class Internal {{ - static readonly {1} x = ({2}){3}; + static readonly {0} x = ({1}){2}; }} """; - return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, languageVersion, valueTypeContainingNamespace, targetType, valueExpression, constantValue, valueType); + return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, targetType, valueExpression, constantValue, valueType); } - public static bool IsAssignableToLocal(Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) + public static bool IsAssignableToLocal(Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) { const string codeTemplate1 = """ - {0} - file static class Internal {{ static void Method() {{ - {1} x = {2}; + {0} x = {1}; }} }} """; const string codeTemplate2 = """ - {0} - file static class Internal {{ static void Method() {{ - {1} x = ({2}){3}; + {0} x = ({1}){2}; }} }} """; - return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, languageVersion, valueTypeContainingNamespace, targetType, valueExpression, constantValue, valueType); + return IsAssignableTo(codeTemplate1, codeTemplate2, compilation, targetType, valueExpression, constantValue, valueType); } - private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, Compilation compilation, LanguageVersion languageVersion, string? valueTypeContainingNamespace, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) + private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) { - var usingDirective = valueTypeContainingNamespace != null ? $"using {valueTypeContainingNamespace};" : ""; - - var hasNoCompilerDiagnostics = HasNoCompilerDiagnostics(string.Format(codeTemplate1, usingDirective, targetType, valueExpression), compilation, languageVersion); - if (hasNoCompilerDiagnostics) + if (valueType == "BenchmarkDotNet.IntegrationTests.ArgumentsTests.WithUndefinedEnumValue.SomeEnum") + { + System.Diagnostics.Debugger.Launch(); + } + var hasCompilerDiagnostics = HasNoCompilerDiagnostics(string.Format(codeTemplate1, targetType, valueExpression), compilation); + if (hasCompilerDiagnostics) { return true; } @@ -212,19 +207,16 @@ private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, C return false; } - return HasNoCompilerDiagnostics(string.Format(codeTemplate2, usingDirective, targetType, valueType, constantLiteral), compilation, languageVersion); + return HasNoCompilerDiagnostics(string.Format(codeTemplate2, targetType, valueType, constantLiteral), compilation); } - private static bool HasNoCompilerDiagnostics(string code, Compilation compilation, LanguageVersion languageVersion) + private static bool HasNoCompilerDiagnostics(string code, Compilation compilation) { - var compilationTestSyntaxTree = CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(languageVersion)); - - var syntaxTreesWithInterceptorsNamespaces = compilation.SyntaxTrees.Where(st => st.Options.Features.ContainsKey(InterceptorsNamespaces)); + var syntaxTree = CSharpSyntaxTree.ParseText(code); var compilerDiagnostics = compilation - .RemoveSyntaxTrees(syntaxTreesWithInterceptorsNamespaces) - .AddSyntaxTrees(compilationTestSyntaxTree) - .GetSemanticModel(compilationTestSyntaxTree) + .AddSyntaxTrees(syntaxTree) + .GetSemanticModel(syntaxTree) .GetMethodBodyDiagnostics() .Where(d => d.DefaultSeverity == DiagnosticSeverity.Error) .ToList(); @@ -260,4 +252,69 @@ public static void Deconstruct(this KeyValuePair tuple, out T1 k key = tuple.Key; value = tuple.Value; } + + public static Location GetLocation(this AttributeData attributeData) + => attributeData.ApplicationSyntaxReference.SyntaxTree.GetLocation(attributeData.ApplicationSyntaxReference.Span); + + public static bool IsAssignable(TypedConstant constant, ExpressionSyntax expression, ITypeSymbol targetType, Compilation compilation) + { + if (constant.IsNull) + { + // Check if targetType is a reference type or nullable. + return targetType.IsReferenceType || targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T; + } + + var sourceType = constant.Type; + if (sourceType == null) + { + return false; + } + + // Test if the constant value is implicitly assignable. + var conversion = compilation.ClassifyConversion(sourceType, targetType); + if (conversion.IsImplicit) + { + return true; + } + + // Int32 values fail the test to smaller types, but it's still valid in the generated code to assign the literal to a smaller integer type, + // so test if the expression is implicitly assignable. + var semanticModel = compilation.GetSemanticModel(expression.SyntaxTree); + conversion = semanticModel.ClassifyConversion(expression, targetType); + return conversion.IsImplicit; + } + + // Assumes a single `params object[] values` constructor + public static ExpressionSyntax GetAttributeParamsArgumentExpression(this AttributeData attributeData, int index) + { + Debug.Assert(index >= 0); + // Properties must come after constructor arguments, so we don't need to worry about it here. + var attrSyntax = (AttributeSyntax) attributeData.ApplicationSyntaxReference.GetSyntax(); + var args = attrSyntax.ArgumentList.Arguments; + Debug.Assert(args is { Count: > 0 }); + var maybeArrayExpression = args[0].Expression; + +#if CODE_ANALYSIS_4_8 + if (maybeArrayExpression is CollectionExpressionSyntax collectionExpressionSyntax) + { + Debug.Assert(index < collectionExpressionSyntax.Elements.Count); + return ((ExpressionElementSyntax) collectionExpressionSyntax.Elements[index]).Expression; + } +#endif + + if (maybeArrayExpression is ArrayCreationExpressionSyntax arrayCreationExpressionSyntax) + { + if (arrayCreationExpressionSyntax.Initializer == null) + { + return maybeArrayExpression; + } + Debug.Assert(index < arrayCreationExpressionSyntax.Initializer.Expressions.Count); + return arrayCreationExpressionSyntax.Initializer.Expressions[index]; + } + + // Params values + Debug.Assert(index < args.Count); + Debug.Assert(args[index].NameEquals is null); + return args[index].Expression; + } } \ No newline at end of file diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md index 27ba8e2573..39c61f7b95 100644 --- a/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md @@ -3,6 +3,12 @@ ### New Rules +Rule ID | Category | Severity | Notes +---------|----------|----------|-------------------- +BDN1503 | Usage | Error | BDN1503_Attributes_ArgumentsAttribute_RequiresParameters + +### New Rules + Rule ID | Category | Severity | Notes ---------|----------|----------|-------------------- BDN1000 | Usage | Error | BDN1000_BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs index 12fb5251c1..33ffb3e35c 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs @@ -4,7 +4,9 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; namespace BenchmarkDotNet.Analyzers.Attributes; @@ -37,11 +39,21 @@ public class ArgumentsAttributeAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description))); + internal static readonly DiagnosticDescriptor RequiresParametersRule = new( + DiagnosticIds.Attributes_ArgumentsAttribute_RequiresParameters, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_RequiresParameters_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_RequiresParameters_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description))); + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] { RequiresBenchmarkAttributeRule, MustHaveMatchingValueCountRule, MustHaveMatchingValueTypeRule, + RequiresParametersRule, }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) @@ -58,17 +70,18 @@ public override void Initialize(AnalysisContext analysisContext) return; } - ctx.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration); + ctx.RegisterSymbolAction(AnalyzeMethodSymbol, SymbolKind.Method); }); } - private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) + private static void AnalyzeMethodSymbol(SymbolAnalysisContext context) { - if (context.Node is not MethodDeclarationSyntax methodDeclarationSyntax) + if (context.Symbol is not IMethodSymbol methodSymbol) { return; } + var benchmarkAttributeTypeSymbol = AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(context.Compilation); var argumentsAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ArgumentsAttribute"); var argumentsSourceAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ArgumentsSourceAttribute"); @@ -77,343 +90,114 @@ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context) 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) + bool hasBenchmarkAttribute = false; + var argumentsAttributes = new List(); + var argumentsSourceAttributes = new List(); + foreach (var attr in methodSymbol.GetAttributes()) { - return; - } - - if (!hasBenchmarkAttribute) - { - foreach (var argumentsAttributeSyntax in argumentsAttributes) + if (attr.AttributeClass.Equals(benchmarkAttributeTypeSymbol)) + { + hasBenchmarkAttribute = true; + } + else if (attr.AttributeClass.Equals(argumentsAttributeTypeSymbol)) + { + argumentsAttributes.Add(attr); + } + else if (attr.AttributeClass.Equals(argumentsSourceAttributeTypeSymbol)) { - context.ReportDiagnostic(Diagnostic.Create(RequiresBenchmarkAttributeRule, argumentsAttributeSyntax.GetLocation())); + argumentsSourceAttributes.Add(attr); } + } + if (argumentsAttributes.Count == 0 && argumentsSourceAttributes.Count == 0) + { return; } - var methodParameterTypeSymbolsBuilder = ImmutableArray.CreateBuilder(methodDeclarationSyntax.ParameterList.Parameters.Count); - - foreach (var parameterSyntax in methodDeclarationSyntax.ParameterList.Parameters) + bool methodHasZeroParams = methodSymbol.Parameters.Length == 0; + if (!hasBenchmarkAttribute || methodHasZeroParams) { - if (parameterSyntax.Type != null) + argumentsAttributes.AddRange(argumentsSourceAttributes); + foreach (var attr in argumentsAttributes) { - var expectedParameterTypeSymbol = context.SemanticModel.GetTypeInfo(parameterSyntax.Type).Type; - if (expectedParameterTypeSymbol != null && expectedParameterTypeSymbol.TypeKind != TypeKind.Error) + if (!hasBenchmarkAttribute) { - methodParameterTypeSymbolsBuilder.Add(expectedParameterTypeSymbol); - - continue; + context.ReportDiagnostic(Diagnostic.Create(RequiresBenchmarkAttributeRule, attr.GetLocation())); + } + if (methodHasZeroParams) + { + context.ReportDiagnostic(Diagnostic.Create(RequiresParametersRule, attr.GetLocation(), methodSymbol.Name)); } - - methodParameterTypeSymbolsBuilder.Add(null); } + return; } - var methodParameterTypeSymbols = methodParameterTypeSymbolsBuilder.ToImmutable(); - - foreach (var argumentsAttributeSyntax in argumentsAttributes) + foreach (var attr in argumentsAttributes) { - if (argumentsAttributeSyntax.ArgumentList == null) + // [Arguments] + if (attr.ConstructorArguments.Length == 0) { - if (methodDeclarationSyntax.ParameterList.Parameters.Count > 0) - { - ReportMustHaveMatchingValueCountDiagnostic(argumentsAttributeSyntax.GetLocation(), 0); - } + ReportMustHaveMatchingValueCountDiagnostic(attr.GetLocation(), 0); + continue; } - 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); - } - } -#if CODE_ANALYSIS_4_8 - // Collection expression - else if (attributeArgumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax) + // [Arguments(null)] + if (attr.ConstructorArguments[0].IsNull) + { + if (methodSymbol.Parameters.Length > 1) { - 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); + ReportMustHaveMatchingValueCountDiagnostic(attr.GetLocation(), 1); } -#endif - - // 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.Node.SyntaxTree, 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.Node.SyntaxTree, - 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.Node.SyntaxTree, argumentsAttributeSyntax.ArgumentList.Arguments.Span), - argumentsAttributeSyntax.ArgumentList.Arguments.Count - ); + var syntax = (AttributeSyntax) attr.ApplicationSyntaxReference.GetSyntax(); + AnalyzeAssignableValueType( + attr.ConstructorArguments[0], + syntax.ArgumentList.Arguments[0].Expression, + methodSymbol.Parameters[0].Type + ); + } + continue; + } - continue; - } + // [Arguments(multiple, values)] + var actualValues = attr.ConstructorArguments[0].Values; + if (actualValues.Length != methodSymbol.Parameters.Length) + { + ReportMustHaveMatchingValueCountDiagnostic(attr.GetLocation(), actualValues.Length); + continue; + } - // ReSharper disable once PossibleNullReferenceException - ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(i => argumentsAttributeSyntax.ArgumentList.Arguments[i].Expression); - } - } - } + for (int i = 0; i < actualValues.Length; i++) + { + AnalyzeAssignableValueType( + actualValues[i], + AnalyzerHelper.GetAttributeParamsArgumentExpression(attr, i), + methodSymbol.Parameters[i].Type + ); } } - return; - void ReportMustHaveMatchingValueCountDiagnostic(Location diagnosticLocation, int valueCount) - { - context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueCountRule, + => context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueCountRule, diagnosticLocation, - methodDeclarationSyntax.ParameterList.Parameters.Count, - methodDeclarationSyntax.ParameterList.Parameters.Count == 1 ? "" : "s", - methodDeclarationSyntax.Identifier.ToString(), + methodSymbol.Parameters.Length, + methodSymbol.Parameters.Length == 1 ? "" : "s", + methodSymbol.Name, valueCount) ); - } - void ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(Func valueExpressionSyntaxFunc) + void AnalyzeAssignableValueType(TypedConstant value, ExpressionSyntax expression, ITypeSymbol parameterType) { - for (var i = 0; i < methodParameterTypeSymbols.Length; i++) + if (!AnalyzerHelper.IsAssignable(value, expression, parameterType, context.Compilation)) { - 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 expectedValueTypeString = methodParameterTypeSymbol.ToString(); - var actualValueTypeSymbol = context.SemanticModel.GetTypeInfo(valueExpressionSyntax).Type; - - if (actualValueTypeSymbol is - { TypeKind: TypeKind.Array - or TypeKind.Class - or TypeKind.Struct - or TypeKind.Enum - }) - { - var actualValueTypeString = actualValueTypeSymbol.ToString(); - - var typeTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Type"); - - if (methodParameterTypeSymbol.Equals(typeTypeSymbol)) - { - if (!actualValueTypeSymbol.Equals(typeTypeSymbol)) - { - ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( - valueExpressionSyntax.GetLocation(), - valueExpressionString, - expectedValueTypeString, - actualValueTypeString - ); - } - - continue; - } - - string? valueTypeContainingNamespace = null; - - if (actualValueTypeSymbol.TypeKind == TypeKind.Enum && !actualValueTypeSymbol.ContainingNamespace.IsGlobalNamespace) - { - valueTypeContainingNamespace = actualValueTypeSymbol.ContainingNamespace.ToString(); - } - - if (actualValueTypeSymbol is IArrayTypeSymbol actualValueArrayTypeSymbol) - { - if (methodParameterTypeSymbol is IArrayTypeSymbol expectedValueArrayTypeSymbol && expectedValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol)) - { - if (!actualValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol)) - { - ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( - valueExpressionSyntax.GetLocation(), - valueExpressionString, - expectedValueTypeString, - actualValueTypeString - ); - } - - continue; - } - - if (actualValueArrayTypeSymbol.ElementType.TypeKind == TypeKind.Enum) - { - if (!actualValueArrayTypeSymbol.ElementType.ContainingNamespace.IsGlobalNamespace) - { - valueTypeContainingNamespace = actualValueArrayTypeSymbol.ElementType.ContainingNamespace.ToString(); - } - } - else if (actualValueArrayTypeSymbol.ElementType.TypeKind == TypeKind.Struct) - { - // Nullable - if (actualValueArrayTypeSymbol.ElementType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - continue; - } - } - - else if (actualValueArrayTypeSymbol.ElementType.TypeKind is not TypeKind.Class) - { - continue; - } - } - - if (!AnalyzerHelper.IsAssignableToLocal(context.Compilation, - (context.Node.SyntaxTree.Options as CSharpParseOptions)!.LanguageVersion, - valueTypeContainingNamespace, - methodParameterTypeSymbol, - valueExpressionString, - constantValue, - actualValueTypeString)) - { - ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( - valueExpressionSyntax.GetLocation(), - valueExpressionString, - methodParameterTypeSymbol.ToString(), - actualValueTypeString - ); - } - } - else if (constantValue is { HasValue: true, Value: null }) - { - if (!AnalyzerHelper.IsAssignableToField(context.Compilation, - (context.Node.SyntaxTree.Options as CSharpParseOptions)!.LanguageVersion, - null, - methodParameterTypeSymbol, - valueExpressionString, - constantValue, - null)) - { - ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( - valueExpressionSyntax.GetLocation(), - valueExpressionString, - expectedValueTypeString, - "null" - ); - } - } + context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueTypeRule, + expression.GetLocation(), + expression.ToString(), + parameterType.ToDisplayString(), + value.IsNull ? "null" : value.Type.ToDisplayString()) + ); } - - 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; - } } \ No newline at end of file diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs index d152fab6b0..a7db949f51 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs @@ -232,110 +232,27 @@ void ReportIfNotImplicitlyConvertibleValueTypeDiagnostic(ExpressionSyntax valueE var valueExpressionString = valueExpressionSyntax.ToString(); - var expectedValueTypeString = expectedValueTypeSymbol.ToString(); var actualValueTypeSymbol = context.SemanticModel.GetTypeInfo(valueExpressionSyntax).Type; - - if (actualValueTypeSymbol is - { TypeKind: TypeKind.Array - or TypeKind.Class - or TypeKind.Struct - or TypeKind.Enum - }) + if (actualValueTypeSymbol != null && actualValueTypeSymbol.TypeKind != TypeKind.Error) { - var actualValueTypeString = actualValueTypeSymbol.ToString(); - - var typeTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Type"); - - if (expectedValueTypeSymbol.Equals(typeTypeSymbol)) - { - if (!actualValueTypeSymbol.Equals(typeTypeSymbol)) - { - ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( - valueExpressionSyntax.GetLocation(), - valueExpressionString, - expectedValueTypeString, - actualValueTypeString - ); - } - - return; - } - - string? valueTypeContainingNamespace = null; - - if (actualValueTypeSymbol.TypeKind == TypeKind.Enum && !actualValueTypeSymbol.ContainingNamespace.IsGlobalNamespace) - { - valueTypeContainingNamespace = actualValueTypeSymbol.ContainingNamespace.ToString(); - } - - if (actualValueTypeSymbol is IArrayTypeSymbol actualValueArrayTypeSymbol) - { - if (expectedValueTypeSymbol is IArrayTypeSymbol expectedValueArrayTypeSymbol && expectedValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol)) - { - if (!actualValueArrayTypeSymbol.ElementType.Equals(typeTypeSymbol)) - { - ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( - valueExpressionSyntax.GetLocation(), - valueExpressionString, - expectedValueTypeString, - actualValueTypeString - ); - } - - return; - } - - if (actualValueArrayTypeSymbol.ElementType.TypeKind == TypeKind.Enum) - { - if (!actualValueArrayTypeSymbol.ElementType.ContainingNamespace.IsGlobalNamespace) - { - valueTypeContainingNamespace = actualValueArrayTypeSymbol.ElementType.ContainingNamespace.ToString(); - } - } - else if (actualValueArrayTypeSymbol.ElementType.TypeKind is TypeKind.Struct) - { - // Nullable - if (actualValueArrayTypeSymbol.ElementType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - return; - } - } - else if (actualValueArrayTypeSymbol.ElementType.TypeKind is not TypeKind.Class) - { - return; - } - } - - if (!AnalyzerHelper.IsAssignableToField(context.Compilation, - (context.Node.SyntaxTree.Options as CSharpParseOptions)!.LanguageVersion, - valueTypeContainingNamespace, - expectedValueTypeSymbol, - valueExpressionString, - constantValue, - actualValueTypeString)) + if (!AnalyzerHelper.IsAssignableToField(context.Compilation, expectedValueTypeSymbol, valueExpressionString, constantValue, actualValueTypeSymbol.ToString())) { ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( valueExpressionSyntax.GetLocation(), valueExpressionString, - expectedValueTypeString, - actualValueTypeString + fieldOrPropertyTypeSyntax.ToString(), + actualValueTypeSymbol.ToString() ); } } else if (constantValue is { HasValue: true, Value: null }) { - if (!AnalyzerHelper.IsAssignableToField(context.Compilation, - (context.Node.SyntaxTree.Options as CSharpParseOptions)!.LanguageVersion, - null, - expectedValueTypeSymbol, - valueExpressionString, - constantValue, - null)) + if (!AnalyzerHelper.IsAssignableToField(context.Compilation, expectedValueTypeSymbol, valueExpressionString, constantValue, null)) { ReportValueTypeMustBeImplicitlyConvertibleDiagnostic( valueExpressionSyntax.GetLocation(), valueExpressionString, - expectedValueTypeString, + fieldOrPropertyTypeSyntax.ToString(), "null" ); } diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs index aa49e5e367..272c028e0d 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs @@ -115,7 +115,7 @@ internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_T } /// - /// Looks up a localized string similar to The [Arguments] attribute can only be used on methods annotated with the [Benchmark] attribute. + /// Looks up a localized string similar to The [Arguments(Source)] attribute can only be used on methods annotated with the [Benchmark] attribute. /// internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_MessageFormat { get { @@ -124,7 +124,7 @@ internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_ } /// - /// Looks up a localized string similar to [Arguments] attribute can only be used on methods annotated with the [Benchmark] attribute. + /// Looks up a localized string similar to [Arguments(Source)] attribute can only be used on methods annotated with the [Benchmark] attribute. /// internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_Title { get { @@ -132,6 +132,24 @@ internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_ } } + /// + /// Looks up a localized string similar to Method {0} has no parameters. + /// + internal static string Attributes_ArgumentsAttribute_RequiresParameters_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresParameters_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Arguments(Source)] attribute requires at least 1 parameter. + /// + internal static string Attributes_ArgumentsAttribute_RequiresParameters_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresParameters_Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to This method declares one or more parameters but is not annotated with either an [ArgumentsSource] attribute or one or more [Arguments] attributes. To ensure correct argument binding, methods with parameters must explicitly be annotated with an [ArgumentsSource] attribute or one or more [Arguments] attributes. ///Either add the [ArgumentsSource] or [Arguments] attribute(s) or remove the parameters.. diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx index a71c0dd64f..277f275f46 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx @@ -356,10 +356,10 @@ Either add the [ArgumentsSource] or [Arguments] attribute(s) or remove the param 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 + [Arguments(Source)] 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 + The [Arguments(Source)] 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 @@ -370,4 +370,10 @@ Either add the [ArgumentsSource] or [Arguments] attribute(s) or remove the param Benchmark methods without an [ArgumentsSource] or [Arguments] attribute(s) cannot declare parameters + + [Arguments(Source)] attribute requires at least 1 parameter + + + Method {0} has no parameters + \ No newline at end of file diff --git a/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs b/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs index a512d637c2..d84af67f41 100644 --- a/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs +++ b/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs @@ -33,4 +33,5 @@ public static class DiagnosticIds public const string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute = "BDN1500"; public const string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount = "BDN1501"; public const string Attributes_ArgumentsAttribute_MustHaveMatchingValueType = "BDN1502"; + public const string Attributes_ArgumentsAttribute_RequiresParameters = "BDN1503"; } diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs index 68fd55d8fa..7ec581a1f6 100644 --- a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs @@ -10,19 +10,15 @@ namespace BenchmarkDotNet.Analyzers.Tests.AnalyzerTests.Attributes; public class ArgumentsAttributeAnalyzerTests { - public class General : AnalyzerTestFixture + public class RequiresParameters : AnalyzerTestFixture { + public RequiresParameters() : base(ArgumentsAttributeAnalyzer.RequiresParametersRule) { } + [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) + public async Task A_method_annotated_with_an_arguments_attribute_and_the_benchmark_attribute_and_having_no_parameters_should_trigger_diagnostic( + [CombinatorialMemberData(nameof(ArgumentsAttributeUsagesWithLocationMarker))] string emptyArgumentsAttributeUsage) { - List emptyArgumentsAttributeUsages = []; - - for (var i = 0; i < attributeUsageMultiplier; i++) - { - emptyArgumentsAttributeUsages.Add(emptyArgumentsAttributeUsage); - } + const string benchmarkMethodName = "BenchmarkMethod"; var testCode = /* lang=c#-test */ $$""" using BenchmarkDotNet.Attributes; @@ -30,8 +26,8 @@ public async Task A_method_annotated_with_an_arguments_attribute_with_no_values_ public class BenchmarkClass { [Benchmark] - {{string.Join("\n", emptyArgumentsAttributeUsages)}} - public void BenchmarkMethod() + {{emptyArgumentsAttributeUsage}} + public void {{benchmarkMethodName}}() { } @@ -40,14 +36,15 @@ public void BenchmarkMethod() TestCode = testCode; + AddDefaultExpectedDiagnostic(benchmarkMethodName); await RunAsync(); } - public static IEnumerable EmptyArgumentsAttributeUsages() + public static IEnumerable ArgumentsAttributeUsagesWithLocationMarker() { - yield return "[Arguments]"; - yield return "[Arguments()]"; - yield return "[Arguments(Priority = 1)]"; + yield return "[{|#0:Arguments|}]"; + yield return "[{|#0:Arguments()|}]"; + yield return "[{|#0:Arguments(Priority = 1)|}]"; string[] nameColonUsages = [ @@ -63,9 +60,11 @@ public static IEnumerable EmptyArgumentsAttributeUsages() string[] attributeUsagesBase = [ - "[Arguments({0}new object[] {{ }}{1})]", - "[Arguments({0}new object[0]{1})]", - "[Arguments({0}[]{1})]", + "Arguments({0}new object[] {{ }}{1})", + "Arguments({0}new object[0]{1})", + "Arguments({0}[]{1})", + "Arguments({0}new object[] {{ 1, 2 }}{1})", + "Arguments({0}[1, 2]{1})", ]; foreach (var attributeUsageBase in attributeUsagesBase) @@ -74,7 +73,7 @@ public static IEnumerable EmptyArgumentsAttributeUsages() { foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) { - yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + yield return $"[{{|#0:{string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage)}|}}]"; } } } @@ -220,8 +219,7 @@ public class BenchmarkClass [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) + [CombinatorialMemberData(nameof(ArgumentsAttributeUsagesWithLocationMarker))] string argumentsAttributeUsage) { const string benchmarkMethodName = "BenchmarkMethod"; @@ -232,25 +230,19 @@ public class BenchmarkClass { [Benchmark] {{argumentsAttributeUsage}} - public void {{benchmarkMethodName}}({{parameterData.Parameters}}) + public void {{benchmarkMethodName}}(string a) { } } """; TestCode = testCode; - AddExpectedDiagnostic(0, parameterData.ParameterCount, parameterData.PluralSuffix, benchmarkMethodName, 2); - AddExpectedDiagnostic(1, parameterData.ParameterCount, parameterData.PluralSuffix, benchmarkMethodName, 3); + AddExpectedDiagnostic(0, 1, "", benchmarkMethodName, 2); + AddExpectedDiagnostic(1, 1, "", benchmarkMethodName, 3); await RunAsync(); } - public static IEnumerable<(string, int, string)> ParameterLists => - [ - ("string a", 1, ""), - ("", 0, "s") - ]; - public static TheoryData ArgumentsAttributeUsages() { return [.. GenerateData()]; @@ -302,8 +294,8 @@ public static TheoryData EmptyArgumentsAttributeUsagesWithLocationMarker static IEnumerable GenerateData() { yield return "[{|#0:Arguments|}]"; - yield return "[Arguments{|#0:()|}]"; - yield return "[Arguments({|#0:Priority = 1|})]"; + yield return "[{|#0:Arguments()|}]"; + yield return "[{|#0:Arguments(Priority = 1)|}]"; string[] nameColonUsages = [ @@ -319,9 +311,9 @@ static IEnumerable GenerateData() string[] attributeUsagesBase = [ - "[Arguments({0}new object[] {{|#0:{{ }}|}}{1})]", - "[Arguments({0}new object[{{|#0:0|}}]{1})]", - "[Arguments({0}{{|#0:[]|}}{1})]", + "Arguments({0}new object[] {{ }}{1})", + "Arguments({0}new object[0]{1})", + "Arguments({0}[]{1})", ]; foreach (var attributeUsageBase in attributeUsagesBase) @@ -330,7 +322,7 @@ static IEnumerable GenerateData() { foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) { - yield return string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage); + yield return $"[{{|#0:{string.Format(attributeUsageBase, nameColonUsage, priorityNamedParameterUsage)}|}}]"; } } } @@ -353,9 +345,9 @@ public static IEnumerable ArgumentsAttributeUsagesWithLocationMarker() string[] attributeUsagesBase = [ - "[Arguments({{|#{1}:{2}|}}{3})]", - "[Arguments({0}new object[] {{ {{|#{1}:{2}|}} }}{3})]", - "[Arguments({0}[ {{|#{1}:{2}|}} ]{3})]" + "Arguments({1}{2})", + "Arguments({0}new object[] {{ {1} }}{2})", + "Arguments({0}[ {1} ]{2})" ]; string[] valueLists = @@ -370,7 +362,7 @@ public static IEnumerable ArgumentsAttributeUsagesWithLocationMarker() { foreach (var priorityNamedParameterUsage in priorityNamedParameterUsages) { - yield return string.Join("\n ", valueLists.Select((vv, i) => string.Format(attributeUsageBase, nameColonUsage, i, vv, priorityNamedParameterUsage))); + yield return string.Join("\n ", valueLists.Select((vv, i) => $"[{{|#{i}:{string.Format(attributeUsageBase, nameColonUsage, vv, priorityNamedParameterUsage)}|}}]")); } } } @@ -504,60 +496,6 @@ public void BenchmarkMethod({{parameters}}) await RunAsync(); } - [Theory, CombinatorialData] - public async Task Providing_an_unknown_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, "dummy_literal, true")}}] - public void BenchmarkMethod(byte a, bool b) - { - - } - } - """; - - TestCode = testCode; - ReferenceDummyAttribute(); - - DisableCompilerDiagnostics(); - - await RunAsync(); - } - - [Theory, CombinatorialData] - public async Task Providing_an_unknown_type_in_typeof_expression_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, "typeof(int), typeof(dummy_literal)")}}] - public void BenchmarkMethod(System.Type a, System.Type b) - { - - } - } - """; - - TestCode = testCode; - ReferenceDummyAttribute(); - - DisableCompilerDiagnostics(); - - await RunAsync(); - } - [Theory, CombinatorialData] public async Task Providing_expected_value_type_should_not_trigger_diagnostic( [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, @@ -940,34 +878,6 @@ public void BenchmarkMethod(System.Span a) 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_unexpected_or_not_implicitly_convertible_value_type_should_trigger_diagnostic( [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, @@ -1328,7 +1238,7 @@ public static IEnumerable ArrayValuesContainerAttributeArgumentEnumerabl ( """ (object)"test_object" - """, "object" ), + """, "string" ), ( "typeof(string)", "System.Type" ), ( "DummyEnum.Value1", "DummyEnum" ) ]; From 0d33e33bf3dd95ebe58c9d326ad69eea13cf1f91 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sun, 16 Nov 2025 22:19:52 -0500 Subject: [PATCH 4/6] Refactored ParamsAttributeAnalyzer to use semantic model. Updated to handle more cases. Reverted unknown types test removal. --- .../AnalyzerHelper.cs | 22 +- .../Attributes/ArgumentsAttributeAnalyzer.cs | 5 + .../Attributes/ParamsAttributeAnalyzer.cs | 223 ++++-------------- .../ArgumentsAttributeAnalyzerTests.cs | 82 +++++++ .../ParamsAttributeAnalyzerTests.cs | 12 +- 5 files changed, 158 insertions(+), 186 deletions(-) diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs index c2d1274069..d39262b311 100644 --- a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs @@ -186,10 +186,6 @@ static void Method() {{ private static bool IsAssignableTo(string codeTemplate1, string codeTemplate2, Compilation compilation, ITypeSymbol targetType, string valueExpression, Optional constantValue, string? valueType) { - if (valueType == "BenchmarkDotNet.IntegrationTests.ArgumentsTests.WithUndefinedEnumValue.SomeEnum") - { - System.Diagnostics.Debugger.Launch(); - } var hasCompilerDiagnostics = HasNoCompilerDiagnostics(string.Format(codeTemplate1, targetType, valueExpression), compilation); if (hasCompilerDiagnostics) { @@ -270,7 +266,7 @@ public static bool IsAssignable(TypedConstant constant, ExpressionSyntax express return false; } - // Test if the constant value is implicitly assignable. + // Test if the constant type is implicitly assignable. var conversion = compilation.ClassifyConversion(sourceType, targetType); if (conversion.IsImplicit) { @@ -280,8 +276,20 @@ public static bool IsAssignable(TypedConstant constant, ExpressionSyntax express // Int32 values fail the test to smaller types, but it's still valid in the generated code to assign the literal to a smaller integer type, // so test if the expression is implicitly assignable. var semanticModel = compilation.GetSemanticModel(expression.SyntaxTree); - conversion = semanticModel.ClassifyConversion(expression, targetType); - return conversion.IsImplicit; + // Only enums use explicit casting, so we test with explicit cast only for enums. See BenchmarkConverter.Map(...). + bool isEnum = targetType.TypeKind == TypeKind.Enum; + // The existing implementation only checks for direct enum type, not Nullable, so we won't check it here either unless BenchmarkConverter gets updated to handle it. + //bool isNullableEnum = + // targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && + // targetType is INamedTypeSymbol named && + // named.TypeArguments.Length == 1 && + // named.TypeArguments[0].TypeKind == TypeKind.Enum; + conversion = semanticModel.ClassifyConversion(expression, targetType, isEnum); + if (conversion.IsImplicit) + { + return true; + } + return isEnum && conversion.IsExplicit; } // Assumes a single `params object[] values` constructor diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs index 33ffb3e35c..9618f5f944 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs @@ -189,6 +189,11 @@ void ReportMustHaveMatchingValueCountDiagnostic(Location diagnosticLocation, int void AnalyzeAssignableValueType(TypedConstant value, ExpressionSyntax expression, ITypeSymbol parameterType) { + // Don't analyze unknown types. + if (value.Kind == TypedConstantKind.Error || parameterType is IErrorTypeSymbol) + { + return; + } if (!AnalyzerHelper.IsAssignable(value, expression, parameterType, context.Compilation)) { context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueTypeRule, diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs index a7db949f51..fd8c3b389c 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; @@ -55,220 +54,98 @@ public override void Initialize(AnalysisContext analysisContext) return; } - ctx.RegisterSyntaxNodeAction(Analyze, SyntaxKind.Attribute); + ctx.RegisterSymbolAction(Analyze, SymbolKind.Field); + ctx.RegisterSymbolAction(Analyze, SymbolKind.Property); }); } - private static void Analyze(SyntaxNodeAnalysisContext context) + private void Analyze(SymbolAnalysisContext context) { - if (context.Node is not AttributeSyntax attributeSyntax) + ITypeSymbol fieldOrPropertyType = context.Symbol switch { - return; - } - - var paramsAttributeTypeSymbol = GetParamsAttributeTypeSymbol(context.Compilation); - - var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; - if (attributeSyntaxTypeSymbol == null || !attributeSyntaxTypeSymbol.Equals(paramsAttributeTypeSymbol)) + IFieldSymbol fieldSymbol => fieldSymbol.Type, + IPropertySymbol propertySymbol => propertySymbol.Type, + _ => null + }; + if (fieldOrPropertyType is null) { return; } - var attributeTarget = attributeSyntax.FirstAncestorOrSelf(n => n is FieldDeclarationSyntax or PropertyDeclarationSyntax); - if (attributeTarget == null) + var paramsAttributeTypeSymbol = GetParamsAttributeTypeSymbol(context.Compilation); + var attrs = context.Symbol.GetAttributes(); + var paramsAttributes = attrs.Where(attr => attr.AttributeClass.Equals(paramsAttributeTypeSymbol)).ToImmutableArray(); + if (paramsAttributes.Length != 1) { + // Don't analyze zero or multiple [Params] (multiple is not legal and already handled by GeneralParameterAttributesAnalyzer). return; } - TypeSyntax fieldOrPropertyTypeSyntax; - - if (attributeTarget is FieldDeclarationSyntax fieldDeclarationSyntax) - { - fieldOrPropertyTypeSyntax = fieldDeclarationSyntax.Declaration.Type; + var attr = paramsAttributes[0]; - } - else if (attributeTarget is PropertyDeclarationSyntax propertyDeclarationSyntax) - { - fieldOrPropertyTypeSyntax = propertyDeclarationSyntax.Type; - } - else + // [Params] + if (attr.ConstructorArguments.Length == 0) { + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, attr.GetLocation())); return; } - AnalyzeFieldOrPropertyTypeSyntax(context, fieldOrPropertyTypeSyntax, attributeSyntax); - } - - private static void AnalyzeFieldOrPropertyTypeSyntax(SyntaxNodeAnalysisContext context, TypeSyntax fieldOrPropertyTypeSyntax, AttributeSyntax attributeSyntax) - { - if (attributeSyntax.ArgumentList == null) + // [Params(null)] + if (attr.ConstructorArguments[0].IsNull) { - context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, attributeSyntax.GetLocation())); - + var syntax = (AttributeSyntax) attr.ApplicationSyntaxReference.GetSyntax(); + AnalyzeAssignableValueType( + attr.ConstructorArguments[0], + syntax.ArgumentList.Arguments[0].Expression, + fieldOrPropertyType + ); return; } - if (!attributeSyntax.ArgumentList.Arguments.Any()) - { - context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, attributeSyntax.ArgumentList.GetLocation())); + var actualValues = attr.ConstructorArguments[0].Values; - return; - } - - if (attributeSyntax.ArgumentList.Arguments.All(aas => aas.NameEquals != null)) + // [Params([ ])] + if (actualValues.Length == 0) { - context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, Location.Create(context.Node.SyntaxTree, attributeSyntax.ArgumentList.Arguments.Span))); - + context.ReportDiagnostic(Diagnostic.Create(MustHaveValuesRule, attr.GetLocation())); return; } - var expectedValueTypeSymbol = context.SemanticModel.GetTypeInfo(fieldOrPropertyTypeSyntax).Type; - if (expectedValueTypeSymbol == null || expectedValueTypeSymbol.TypeKind == TypeKind.Error) + // [Params(singleValue)] + if (actualValues.Length == 1) { - return; + context.ReportDiagnostic(Diagnostic.Create(UnnecessarySingleValuePassedToAttributeRule, AnalyzerHelper.GetAttributeParamsArgumentExpression(attr, 0).GetLocation())); } - - // Check if this is an explicit params array creation - - var attributeArgumentSyntax = attributeSyntax.ArgumentList.Arguments.First(); - if (attributeArgumentSyntax.NameEquals != null) + // [Params(multiple, values)] + for (int i = 0; i < actualValues.Length; i++) { - // Ignore named arguments, e.g. Priority - return; + AnalyzeAssignableValueType( + actualValues[i], + AnalyzerHelper.GetAttributeParamsArgumentExpression(attr, i), + fieldOrPropertyType + ); } -#if CODE_ANALYSIS_4_8 - // Collection expression - if (attributeArgumentSyntax.Expression is CollectionExpressionSyntax collectionExpressionSyntax) + void AnalyzeAssignableValueType(TypedConstant value, ExpressionSyntax expression, ITypeSymbol parameterType) { - if (!collectionExpressionSyntax.Elements.Any()) + // Don't analyze unknown types. + if (value.Kind == TypedConstantKind.Error || parameterType is IErrorTypeSymbol) { - 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; - } -#endif - - // 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); - } - - 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" - ); - } - } - - void ReportValueTypeMustBeImplicitlyConvertibleDiagnostic(Location diagnosticLocation, string value, string expectedType, string actualType) + if (!AnalyzerHelper.IsAssignable(value, expression, parameterType, context.Compilation)) { context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueTypeRule, - diagnosticLocation, - value, - expectedType, - actualType) + expression.GetLocation(), + expression.ToString(), + parameterType.ToDisplayString(), + value.IsNull ? "null" : value.Type.ToDisplayString()) ); } } } - private static INamedTypeSymbol? GetParamsAttributeTypeSymbol(Compilation compilation) => compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsAttribute"); + private static INamedTypeSymbol? GetParamsAttributeTypeSymbol(Compilation compilation) + => compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsAttribute"); } \ 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 index 7ec581a1f6..52b4cf8b7a 100644 --- a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs @@ -496,6 +496,60 @@ public void BenchmarkMethod({{parameters}}) await RunAsync(); } + [Theory, CombinatorialData] + public async Task Providing_an_unknown_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, "dummy_literal, true")}}] + public void BenchmarkMethod(byte a, bool b) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + DisableCompilerDiagnostics(); + + await RunAsync(); + } + + [Theory, CombinatorialData] + public async Task Providing_an_unknown_type_in_typeof_expression_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, "typeof(int), typeof(dummy_literal)")}}] + public void BenchmarkMethod(System.Type a, System.Type b) + { + + } + } + """; + + TestCode = testCode; + ReferenceDummyAttribute(); + + DisableCompilerDiagnostics(); + + await RunAsync(); + } + [Theory, CombinatorialData] public async Task Providing_expected_value_type_should_not_trigger_diagnostic( [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, @@ -878,6 +932,34 @@ public void BenchmarkMethod(System.Span a) 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_unexpected_or_not_implicitly_convertible_value_type_should_trigger_diagnostic( [CombinatorialMemberData(nameof(DummyAttributeUsage))] string dummyAttributeUsage, diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs index b916c1f5f3..69cd77b962 100644 --- a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs @@ -138,8 +138,8 @@ public static IEnumerable ScalarValuesContainerAttributeArgument public static IEnumerable EmptyParamsAttributeUsagesWithLocationMarker() { yield return "{|#0:Params|}"; - yield return "Params{|#0:()|}"; - yield return "Params({|#0:Priority = 1|})"; + yield return "{|#0:Params()|}"; + yield return "{|#0:Params(Priority = 1)|}"; string[] nameColonUsages = [ @@ -155,9 +155,9 @@ public static IEnumerable EmptyParamsAttributeUsagesWithLocationMarker() string[] attributeUsagesBase = [ - "Params({0}new object[] {{|#0:{{ }}|}}{1})", - "Params({0}{{|#0:new object[0]|}}{1})", - "Params({0}{{|#0:[ ]|}}{1})", + "{{|#0:Params({0}new object[] {{ }}{1})|}}", + "{{|#0:Params({0}new object[0]{1})|}}", + "{{|#0:Params({0}[ ]{1})|}}", ]; foreach (var attributeUsageBase in attributeUsagesBase) @@ -1096,7 +1096,7 @@ public static IEnumerable> ArrayValuesContainer ( """ (object)"test_object" - """, "object" ), + """, "string" ), ( "typeof(string)", "System.Type" ), ( "DummyEnum.Value1", "DummyEnum" ) ]; From 322269ec28d506dcf80e021ba8851143338ed885 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Sun, 16 Nov 2025 22:32:55 -0500 Subject: [PATCH 5/6] Remove unused code. --- .../AnalyzerHelper.cs | 132 ------------------ 1 file changed, 132 deletions(-) diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs index d39262b311..ee0f3fd842 100644 --- a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs @@ -19,9 +19,6 @@ public static LocalizableResourceString GetResourceString(string name) public static INamedTypeSymbol? GetBenchmarkAttributeTypeSymbol(Compilation compilation) => compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.BenchmarkAttribute"); - public static bool AttributeListsContainAttribute(string attributeName, Compilation compilation, SyntaxList 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) @@ -94,38 +91,6 @@ public static ImmutableArray GetAttributes(INamedTypeSymbol? at return attributesBuilder.ToImmutable(); } - public static int GetAttributeUsageCount(string attributeName, Compilation compilation, SyntaxList attributeLists, SemanticModel semanticModel) - => GetAttributeUsageCount(compilation.GetTypeByMetadataName(attributeName), attributeLists, semanticModel); - - public static int GetAttributeUsageCount(INamedTypeSymbol? attributeTypeSymbol, SyntaxList attributeLists, SemanticModel semanticModel) - { - var attributeUsageCount = 0; - - if (attributeTypeSymbol == null) - { - return 0; - } - - foreach (var attributeListSyntax in attributeLists) - { - foreach (var attributeSyntax in attributeListSyntax.Attributes) - { - var attributeSyntaxTypeSymbol = semanticModel.GetTypeInfo(attributeSyntax).Type; - if (attributeSyntaxTypeSymbol == null) - { - continue; - } - - if (attributeSyntaxTypeSymbol.Equals(attributeTypeSymbol)) - { - attributeUsageCount++; - } - } - } - - return attributeUsageCount; - } - public static string NormalizeTypeName(INamedTypeSymbol namedTypeSymbol) { string typeName; @@ -146,103 +111,6 @@ public static string NormalizeTypeName(INamedTypeSymbol namedTypeSymbol) 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 hasCompilerDiagnostics = HasNoCompilerDiagnostics(string.Format(codeTemplate1, targetType, valueExpression), compilation); - if (hasCompilerDiagnostics) - { - return true; - } - - if (!constantValue.HasValue || valueType == null) - { - return false; - } - - var constantLiteral = FormatLiteral(constantValue.Value); - if (constantLiteral == null) - { - return false; - } - - return HasNoCompilerDiagnostics(string.Format(codeTemplate2, targetType, valueType, constantLiteral), compilation); - } - - private static bool HasNoCompilerDiagnostics(string code, Compilation compilation) - { - var syntaxTree = CSharpSyntaxTree.ParseText(code); - - var compilerDiagnostics = compilation - .AddSyntaxTrees(syntaxTree) - .GetSemanticModel(syntaxTree) - .GetMethodBodyDiagnostics() - .Where(d => d.DefaultSeverity == DiagnosticSeverity.Error) - .ToList(); - - return compilerDiagnostics.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 - }; - } - public static void Deconstruct(this KeyValuePair tuple, out T1 key, out T2 value) { key = tuple.Key; From 55c6ff296137d397f8e5e14fc359d04149ceb8c8 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Mon, 17 Nov 2025 01:01:32 -0500 Subject: [PATCH 6/6] Remove unnecessary pre-release MCC version. --- build/BenchmarkDotNet.Build/Runners/BuildRunner.cs | 2 +- .../BenchmarkDotNet.Analyzers.csproj | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs index 4998f2e0db..3cd01eb5aa 100644 --- a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs +++ b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs @@ -67,7 +67,7 @@ public void BuildProjectSilent(FilePath projectFile) public void BuildAnalyzers() { context.Information("BuildSystemProvider: " + context.BuildSystem().Provider); - string[] mccVersions = ["2.8", "3.8", "4.8", "5.0"]; + string[] mccVersions = ["2.8", "3.8", "4.8"]; foreach (string version in mccVersions) { context.DotNetBuild(context.AnalyzersProjectFile.FullPath, new DotNetBuildSettings diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj index 9162a8ae7d..313b460672 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj @@ -8,13 +8,11 @@ true $(NoWarn);CS1591 - 4.8 bin\$(Configuration)\roslyn$(MccVersion)\cs false $(DefineConstants);CODE_ANALYSIS_3_8 $(DefineConstants);CODE_ANALYSIS_4_8 - $(DefineConstants);CODE_ANALYSIS_5_0 $(NoWarn);RS1024;RS2007 @@ -29,8 +27,6 @@ - -