From 4e0e3b9232e1f4d977ea678203f2aa06bbb698f9 Mon Sep 17 00:00:00 2001 From: Tim Cassell Date: Fri, 4 Sep 2026 21:17:07 -0400 Subject: [PATCH] Add async enumerable support to `ParamsSource`/`ArgumentsSource`. Refactored parameter discovery and codegen. Added/removed analyzer rules. Added tests. Co-Authored-By: Claude Opus 5 --- BenchmarkDotNet.slnx | 1 + .../Runners/BuildRunner.cs | 8 +- build/roslynBands.props | 26 + docs/articles/features/parameterization.md | 2 +- docs/articles/samples/IntroArgumentsSource.md | 11 +- docs/articles/samples/IntroParamsSource.md | 28 +- .../IntroArgumentsSource.cs | 16 + .../IntroParamsSource.cs | 18 +- .../AnalyzerHelper.cs | 263 +- .../AnalyzerReleases.Unshipped.md | 15 +- .../AsyncTypeShapes.cs | 83 + .../Attributes/ArgumentsAttributeAnalyzer.cs | 141 +- .../BenchmarkCancellationAttributeAnalyzer.cs | 2 +- .../GeneralParameterAttributesAnalyzer.cs | 225 +- .../ParamsAllValuesAttributeAnalyzer.cs | 2 +- .../Attributes/ParamsAttributeAnalyzer.cs | 10 +- .../SetupCleanupAsyncEnumerableAnalyzer.cs | 2 +- .../BenchmarkDotNet.Analyzers.csproj | 36 +- ...nchmarkDotNetAnalyzerResources.Designer.cs | 2321 +++++++++-------- .../BenchmarkDotNetAnalyzerResources.resx | 987 +++---- .../BenchmarkRunner/RunAnalyzer.cs | 2 +- .../DiagnosticIds.cs | 11 +- .../General/AsyncBenchmarkAnalyzer.cs | 4 +- ...aitableAsyncEnumerableAmbiguityAnalyzer.cs | 2 +- .../General/BenchmarkClassAnalyzer.cs | 40 +- .../Polyfills/RefLikeTypePolyfill.cs | 41 + .../Polyfills/SymbolEqualityComparer.cs | 19 + .../RequiredMemberAnalyzer.cs | 200 ++ .../BenchmarkDotNet.CodeFixers.csproj | 25 +- .../EtwProfiler.cs | 12 +- src/BenchmarkDotNet/BenchmarkDotNet.csproj | 3 +- src/BenchmarkDotNet/Code/ArrayParam.cs | 86 - .../Code/CSharpParameterRenderer.cs | 180 ++ src/BenchmarkDotNet/Code/CodeGenerator.cs | 146 +- .../Code/DeclarationsProvider.cs | 82 +- src/BenchmarkDotNet/Code/EnumParam.cs | 60 - src/BenchmarkDotNet/Code/IParam.cs | 23 - src/BenchmarkDotNet/Code/RunnableConstants.cs | 63 + src/BenchmarkDotNet/Configs/DefaultConfig.cs | 2 + .../Configs/ImmutableConfigBuilder.cs | 2 + .../ConsoleArguments/CorrectionsSuggester.cs | 16 +- .../Diagnosers/CompositeDiagnoser.cs | 19 +- .../Disassemblers/ClrMdArgs.cs | 3 +- .../Disassemblers/ClrMdDisassembler.cs | 3 +- .../Disassemblers/DataContracts.cs | 5 - .../Disassemblers/DisassemblyDiagnoser.cs | 3 +- .../BenchmarkSynchronizationContext.cs | 9 + .../Exporters/FullNameProvider.cs | 2 - .../Extensions/Polyfills/AsyncEnumerable.cs | 40 + .../Extensions/ReflectionExtensions.cs | 291 ++- src/BenchmarkDotNet/Helpers/DisposeHelper.cs | 31 + .../Helpers/DynamicAwaitHelper.cs | 130 +- .../Helpers/SourceCodeHelper.cs | 12 +- .../Parameters/ArrayDisplay.cs | 32 + .../Parameters/ParameterDefinition.cs | 35 +- .../Parameters/ParameterDefinitions.cs | 36 - .../Parameters/ParameterInstance.cs | 76 +- .../Parameters/ParameterInstances.cs | 53 +- .../Parameters/ParameterValue.cs | 39 + .../Parameters/ParameterValues.cs | 17 + .../Parameters/SmartParamBuilder.cs | 212 +- src/BenchmarkDotNet/Parameters/SourceRead.cs | 22 + src/BenchmarkDotNet/Running/BenchmarkCase.cs | 4 +- .../Running/BenchmarkConverter.cs | 267 +- .../Running/BenchmarkRunInfo.cs | 12 +- .../Running/BenchmarkRunnerClean.cs | 65 +- .../Running/BenchmarkRunnerDirty.cs | 18 +- .../Running/BenchmarkSwitcher.cs | 18 +- .../Running/IUserInteraction.cs | 2 +- src/BenchmarkDotNet/Running/TypeFilter.cs | 21 +- .../Running/UserInteraction.cs | 4 +- .../Templates/BenchmarkProgram.txt | 21 +- .../Templates/BenchmarkType.txt | 106 +- src/BenchmarkDotNet/Templates/CsProj.txt | 2 +- .../Templates/MonoAOTLLVMCsProj.txt | 2 +- src/BenchmarkDotNet/Templates/R2RCsProj.txt | 2 +- src/BenchmarkDotNet/Templates/WasmCsProj.txt | 2 +- .../Emitters/AsyncCoreEmitter.cs | 2 +- .../Emitters/AsyncCoreEmitterBase.cs | 2 +- .../Emitters/AsyncEnumerableCoreEmitter.cs | 2 +- .../Emitters/AsyncStateMachineEmitter.cs | 87 +- .../Emitters/RunnableEmitter.cs | 6 +- .../Emitters/SyncCoreEmitter.cs | 2 +- .../Emitters/SyncTaskCoreEmitter.cs | 17 +- .../Runnable/RunnableConstants.cs | 34 - .../Runnable/RunnableReflectionHelpers.cs | 26 +- .../InProcess/Emit/InProcessEmitGenerator.cs | 2 +- .../InProcess/Emit/InProcessEmitRunner.cs | 6 +- .../InProcess/NoEmit/InProcessNoEmitRunner.cs | 43 +- .../Toolchains/NativeAot/Generator.cs | 2 +- .../Toolchains/Roslyn/Builder.cs | 2 +- .../BenchmarkCancellationValidator.cs | 9 +- .../Validators/CompositeValidator.cs | 28 +- .../Validators/DiagnosersValidator.cs | 24 +- .../Validators/ExecutionValidator.cs | 33 +- .../Validators/ExecutionValidatorBase.cs | 19 +- .../Validators/ParamsAllValuesValidator.cs | 4 +- .../Validators/ParamsValidator.cs | 31 +- .../Validators/RequiredMemberValidator.cs | 62 + .../Validators/ReturnValueValidator.cs | 30 +- .../Validators/SourceReturnTypeValidator.cs | 73 + .../ArgumentsAttributeAnalyzerTests.cs | 499 ++++ ...GeneralParameterAttributesAnalyzerTests.cs | 976 +++++-- .../ParamsAttributeAnalyzerTests.cs | 53 + .../General/BenchmarkClassAnalyzerTests.cs | 124 + .../RequiredMemberAnalyzerTests.cs | 326 +++ .../FinalizerBlockerDiagnoser.cs | 10 +- .../ArgumentsTests.cs | 82 + .../AsyncEnumerableParamsSourceTests.cs | 273 ++ .../AttributesTests.cs | 2 +- .../BenchmarkDotNet.IntegrationTests.csproj | 2 +- .../BenchmarkSwitcherTest.cs | 6 +- .../CancellationTokenTests.cs | 72 + .../ConflictingNamesTests.cs | 27 +- .../NaiveRunnableEmitDiff.cs | 12 +- .../RunnableRefArgsFromSourceBenchmark.cs | 39 + .../InProcessEmitTest.cs | 5 + .../ParamSourceTests.cs | 237 +- .../ParamsTests.cs | 2 +- .../BenchmarkDotNet.Tests.csproj | 2 +- .../Columns/MetricColumnTests.cs | 2 +- .../Exporters/OpenMetricsExporterTests.cs | 34 +- .../Helpers/DynamicAwaitHelperTests.cs | 304 +++ .../Order/DefaultOrdererTests.cs | 2 +- .../ParameterComparerTests.cs | 2 +- .../ParameterInstanceTests.cs | 2 +- .../ParamsSourceTests.cs | 697 +++++ .../RefStructSourceTests.cs | 124 + .../BenchmarkDotNet.Tests/ReflectionTests.cs | 50 + .../Polyfills/AsyncEnumerableExtensions.cs | 38 + .../Validators/ExecutionValidatorTests.cs | 102 +- .../Validators/ParamsValidatorTests.cs | 43 +- .../RequiredMemberValidatorTests.cs | 105 + .../Validators/ReturnValueValidatorTests.cs | 82 +- .../SourceReturnTypeValidatorTests.cs | 309 +++ .../SynchronizationContextCaptureTests.cs | 139 + 136 files changed, 9270 insertions(+), 2892 deletions(-) create mode 100644 build/roslynBands.props create mode 100644 src/BenchmarkDotNet.Analyzers/Polyfills/RefLikeTypePolyfill.cs create mode 100644 src/BenchmarkDotNet.Analyzers/Polyfills/SymbolEqualityComparer.cs create mode 100644 src/BenchmarkDotNet.Analyzers/RequiredMemberAnalyzer.cs delete mode 100644 src/BenchmarkDotNet/Code/ArrayParam.cs create mode 100644 src/BenchmarkDotNet/Code/CSharpParameterRenderer.cs delete mode 100644 src/BenchmarkDotNet/Code/EnumParam.cs delete mode 100644 src/BenchmarkDotNet/Code/IParam.cs create mode 100644 src/BenchmarkDotNet/Code/RunnableConstants.cs create mode 100644 src/BenchmarkDotNet/Extensions/Polyfills/AsyncEnumerable.cs create mode 100644 src/BenchmarkDotNet/Helpers/DisposeHelper.cs create mode 100644 src/BenchmarkDotNet/Parameters/ArrayDisplay.cs delete mode 100644 src/BenchmarkDotNet/Parameters/ParameterDefinitions.cs create mode 100644 src/BenchmarkDotNet/Parameters/ParameterValue.cs create mode 100644 src/BenchmarkDotNet/Parameters/ParameterValues.cs create mode 100644 src/BenchmarkDotNet/Parameters/SourceRead.cs delete mode 100644 src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs create mode 100644 src/BenchmarkDotNet/Validators/RequiredMemberValidator.cs create mode 100644 src/BenchmarkDotNet/Validators/SourceReturnTypeValidator.cs create mode 100644 tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/RequiredMemberAnalyzerTests.cs create mode 100644 tests/BenchmarkDotNet.IntegrationTests/AsyncEnumerableParamsSourceTests.cs create mode 100644 tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/RunnableRefArgsFromSourceBenchmark.cs create mode 100644 tests/BenchmarkDotNet.Tests/Helpers/DynamicAwaitHelperTests.cs create mode 100644 tests/BenchmarkDotNet.Tests/RefStructSourceTests.cs create mode 100644 tests/BenchmarkDotNet.Tests/Shared/Polyfills/AsyncEnumerableExtensions.cs create mode 100644 tests/BenchmarkDotNet.Tests/Validators/RequiredMemberValidatorTests.cs create mode 100644 tests/BenchmarkDotNet.Tests/Validators/SourceReturnTypeValidatorTests.cs create mode 100644 tests/BenchmarkDotNet.Tests/Validators/SynchronizationContextCaptureTests.cs diff --git a/BenchmarkDotNet.slnx b/BenchmarkDotNet.slnx index 2d5a3cf0dd..3e9ab21930 100644 --- a/BenchmarkDotNet.slnx +++ b/BenchmarkDotNet.slnx @@ -2,6 +2,7 @@ + diff --git a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs index 12f7531cff..df56308025 100644 --- a/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs +++ b/build/BenchmarkDotNet.Build/Runners/BuildRunner.cs @@ -124,12 +124,14 @@ public void BuildProjectSilent(FilePath projectFile) public void BuildAnalyzers() { context.Information("BuildSystemProvider: " + context.BuildSystem().Provider); - string[] mccVersions = ["2.8", "3.8", "4.8"]; + // Each band must restore as well as build: the PackageReference is chosen by MccVersion, so reusing the + // default band's project.assets.json silently compiles every band against the default's Roslyn. + string[] mccVersions = ["2.8", "3.0", "3.8", "4.4", "4.8", "4.12"]; foreach (string version in mccVersions) { context.DotNetBuild(context.AnalyzersProjectFile.FullPath, new DotNetBuildSettings { - NoRestore = true, + NoRestore = false, DiagnosticOutput = true, MSBuildSettings = context.MsBuildSettingsBuild, Configuration = context.BuildConfiguration, @@ -139,7 +141,7 @@ public void BuildAnalyzers() context.DotNetBuild(context.CodeFixersProjectFile.FullPath, new DotNetBuildSettings { - NoRestore = true, + NoRestore = false, DiagnosticOutput = true, MSBuildSettings = context.MsBuildSettingsBuild, Configuration = context.BuildConfiguration, diff --git a/build/roslynBands.props b/build/roslynBands.props new file mode 100644 index 0000000000..07375d64ef --- /dev/null +++ b/build/roslynBands.props @@ -0,0 +1,26 @@ + + + + netstandard2.0 + false + true + $(NoWarn);CS1591 + + 4.12 + bin\$(Configuration)\roslyn$(MccVersion)\cs + false + + + + $(DefineConstants);CODE_ANALYSIS_3_0 + + $(DefineConstants);CODE_ANALYSIS_3_8 + + $(DefineConstants);CODE_ANALYSIS_4_4 + + $(DefineConstants);CODE_ANALYSIS_4_8 + + $(DefineConstants);CODE_ANALYSIS_4_12 + $(MccVersion).0 + + diff --git a/docs/articles/features/parameterization.md b/docs/articles/features/parameterization.md index eecfd29178..cbf03da4a5 100644 --- a/docs/articles/features/parameterization.md +++ b/docs/articles/features/parameterization.md @@ -21,4 +21,4 @@ name: Benchmark Parameterization [!include[IntroArrayParam](../samples/IntroArrayParam.md)] -[!include[IntroArguments](../samples/IntroArgumentsPriority.md)] +[!include[IntroArgumentsPriority](../samples/IntroArgumentsPriority.md)] diff --git a/docs/articles/samples/IntroArgumentsSource.md b/docs/articles/samples/IntroArgumentsSource.md index 62340b3a33..3ab707e127 100644 --- a/docs/articles/samples/IntroArgumentsSource.md +++ b/docs/articles/samples/IntroArgumentsSource.md @@ -10,9 +10,18 @@ In case you want to use a lot of values, you should use You can mark one or several fields or properties in your class by the [`[ArgumentsSource]`](xref:BenchmarkDotNet.Attributes.ArgumentsSourceAttribute) attribute. In this attribute, you have to specify the name of public method/property which is going to provide the values - (something that implements `IEnumerable`). + (something that implements `IEnumerable` or `IAsyncEnumerable`). +The element type has to be named: a source declared to return only the non-generic `IEnumerable` is rejected, + because the generated code has nothing to infer the argument's type from. The source may be instance or static. If the source is not in the same type as the benchmark, the type containing the source must be specified in the attribute constructor. +A source returning `IAsyncEnumerable` is awaited while the values are read, so they can be produced + asynchronously without resorting to blocking sync-over-async in the source, and such a source method may take + an optional [`[EnumeratorCancellation]`](xref:System.Runtime.CompilerServices.EnumeratorCancellationAttribute) + `CancellationToken` parameter. Starting the run from a thread that carries a single-threaded + `SynchronizationContext` needs the asynchronous entry points - see + @BenchmarkDotNet.Samples.IntroParamsSource, where the same applies to `[ParamsSource]`. + ### Source code [!code-csharp[IntroArgumentsSource.cs](../../../samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs)] diff --git a/docs/articles/samples/IntroParamsSource.md b/docs/articles/samples/IntroParamsSource.md index e631fbf1db..29ca62c340 100644 --- a/docs/articles/samples/IntroParamsSource.md +++ b/docs/articles/samples/IntroParamsSource.md @@ -7,10 +7,29 @@ uid: BenchmarkDotNet.Samples.IntroParamsSource In case you want to use a lot of values, you should use [`[ParamsSource]`](xref:BenchmarkDotNet.Attributes.ParamsSourceAttribute) You can mark one or several fields or properties in your class by the - [`[Params]`](xref:BenchmarkDotNet.Attributes.ParamsAttribute) attribute. + [`[ParamsSource]`](xref:BenchmarkDotNet.Attributes.ParamsSourceAttribute) attribute. In this attribute, you have to specify the name of public method/property which is going to provide the values - (something that implements `IEnumerable`). + (something that implements `IEnumerable` or `IAsyncEnumerable`). +The element type has to be named: a source declared to return only the non-generic `IEnumerable` is rejected, + because the generated code has nothing to infer the parameter's type from. The source may be instance or static. If the source is not in the same type as the benchmark, the type containing the source must be specified in the attribute constructor. +A static source declared on a base type is used just as one declared on the benchmark type itself. + +A source returning `IAsyncEnumerable` is awaited while the values are read, so they can be produced + asynchronously - loaded from a database or a remote service, say - without resorting to blocking + sync-over-async in the source. Such a source method may take an optional + [`[EnumeratorCancellation]`](xref:System.Runtime.CompilerServices.EnumeratorCancellationAttribute) + `CancellationToken` parameter, which receives the benchmark's cancellation token while the values are + enumerated, so the asynchronous work can be cancelled. + +If you start the run from a thread that carries a single-threaded `SynchronizationContext` - a WPF or + WinForms UI thread, or legacy ASP.NET - use the asynchronous entry points + ([`BenchmarkRunner.RunAsync`](xref:BenchmarkDotNet.Running.BenchmarkRunner) or + [`BenchmarkConverter.TypeToBenchmarksAsync`](xref:BenchmarkDotNet.Running.BenchmarkConverter)) and await + them. The synchronous ones block the calling thread while the values are read, so an `await` inside your + own source captures that context and its continuation cannot run until the call it is blocking returns. + Awaiting the asynchronous entry point leaves the thread free to run it. Writing the source's own awaits as + `ConfigureAwait(false)` avoids the capture as well. ### Source code @@ -29,10 +48,7 @@ The source may be instance or static. If the source is not in the same type as t ### Remarks -**A remark about IParam.** - -You don't need to use `IParam` anymore since `0.11.0`. -Just use complex types as you wish and override `ToString` method to change the display names used in the results. +Use complex types as you wish and override the `ToString` method to change the display names used in the results. ### Links diff --git a/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs b/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs index 273c1994a3..971c4d081c 100644 --- a/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs +++ b/samples/BenchmarkDotNet.Samples/IntroArgumentsSource.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using BenchmarkDotNet.Attributes; namespace BenchmarkDotNet.Samples @@ -19,6 +20,21 @@ public class IntroArgumentsSource [Benchmark] [ArgumentsSource(typeof(BenchmarkArguments), nameof(BenchmarkArguments.TimeSpans))] // when the arguments come from a different type, specify that type here public void SingleArgument(TimeSpan time) => Thread.Sleep(time); + + [Benchmark] + [ArgumentsSource(nameof(NumbersAsync))] + public double AsyncSourcedArguments(double x, double y) => Math.Pow(x, y); + + // the source may be an IAsyncEnumerable, which BenchmarkDotNet awaits, so the values can be produced + // asynchronously without resorting to blocking sync-over-async in the source. It may take an optional + // [EnumeratorCancellation] CancellationToken, which receives the benchmark's cancellation token while + // the values are enumerated. + public static async IAsyncEnumerable NumbersAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Delay(10, cancellationToken); + yield return new object[] { 1.0, 1.0 }; + yield return new object[] { 2.0, 2.0 }; + } } public static class BenchmarkArguments diff --git a/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs b/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs index 412728c226..64c7b8d6e1 100644 --- a/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs +++ b/samples/BenchmarkDotNet.Samples/IntroParamsSource.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using BenchmarkDotNet.Attributes; namespace BenchmarkDotNet.Samples @@ -22,8 +23,23 @@ public class IntroParamsSource [ParamsSource(typeof(ParamsValues), nameof(ParamsValues.ValuesForC))] public int C; + // public field getting its params from an asynchronous source, which BenchmarkDotNet awaits. + // Useful when the values can only be produced asynchronously - loaded from a database or a remote + // service - without resorting to blocking sync-over-async in the source. + [ParamsSource(nameof(ValuesForD))] + public int D; + + // the source method may take an optional [EnumeratorCancellation] CancellationToken. It receives the + // benchmark's cancellation token while the values are enumerated, so the async work can be cancelled. + public static async IAsyncEnumerable ValuesForD([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.Delay(10, cancellationToken); + yield return 1; + yield return 2; + } + [Benchmark] - public void Benchmark() => Thread.Sleep(A + B + C + 5); + public void Benchmark() => Thread.Sleep(A + B + C + D + 5); } public static class ParamsValues diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs index 4351a38281..3a5450691c 100644 --- a/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerHelper.cs @@ -13,9 +13,188 @@ internal static class AnalyzerHelper public static LocalizableResourceString GetResourceString(string name) => new(name, BenchmarkDotNetAnalyzerResources.ResourceManager, typeof(BenchmarkDotNetAnalyzerResources)); + // Shared by the [ParamsSource] and [ArgumentsSource] analyzers: the runtime only invokes a source method whose + // parameters are all optional, so a method with a required parameter isn't recognized as a source. + public static readonly DiagnosticDescriptor SourceMethodMustNotHaveRequiredParametersRule = new( + DiagnosticIds.General_Source_MethodMustNotHaveRequiredParameters, + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotHaveRequiredParameters_Title)), + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotHaveRequiredParameters_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotHaveRequiredParameters_Description))); + + // Shared by the [ParamsSource] and [ArgumentsSource] analyzers: the runtime invokes a source method directly, + // and nothing supplies a generic one's type arguments. + public static readonly DiagnosticDescriptor SourceMethodMustNotBeGenericRule = new( + DiagnosticIds.General_Source_MethodMustNotBeGeneric, + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotBeGeneric_Title)), + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotBeGeneric_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_MethodMustNotBeGeneric_Description))); + + // Shared by both source analyzers: discovery reads a source's values into an object[], and a ref struct cannot + // be boxed. Expressible since .NET 10 gave IEnumerable an allows-ref-struct type parameter. + public static readonly DiagnosticDescriptor SourceElementMustNotBeByRefLikeRule = new( + DiagnosticIds.General_Source_ElementMustNotBeByRefLike, + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMustNotBeByRefLike_Title)), + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMustNotBeByRefLike_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMustNotBeByRefLike_Description))); + + // The constraint case, which the compiler cannot decide: a type argument that is not by-ref-like reads + // normally, so this warns where the rule above - a ref struct the compiler can see - is an error. + public static readonly DiagnosticDescriptor SourceElementMayBeByRefLikeRule = new( + DiagnosticIds.General_Source_ElementMayBeByRefLike, + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMayBeByRefLike_Title)), + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMayBeByRefLike_MessageFormat)), + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_ElementMayBeByRefLike_Description))); + + // Shared by both source analyzers: a source that is both shapes cannot be read unambiguously - discovery + // takes the synchronous path while the generated code cannot pick a GetParameterAsync overload. + public static readonly DiagnosticDescriptor SourceMustNotBeAmbiguouslyEnumerableRule = new( + DiagnosticIds.General_Source_AmbiguousEnumerableShape, + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_AmbiguousEnumerableShape_Title)), + GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_AmbiguousEnumerableShape_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_Source_AmbiguousEnumerableShape_Description))); + + // Mirrors GetValidValuesForParamsSourceAsync, which passes over generic methods. True only when a public + // generic method of that name exists and nothing else can serve it - the runtime would have nothing to invoke. + public static bool SourceResolvesOnlyToGenericMethod(ITypeSymbol type, string name) + { + bool anyGenericMethod = false; + bool anyInvocableMethod = false; + bool anyReadableProperty = false; + + for (ITypeSymbol? current = type; current != null; current = current.BaseType) + { + foreach (var member in current.GetMembers(name)) + { + if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary && method.DeclaredAccessibility == Accessibility.Public) + { + if (method.IsGenericMethod) + { + anyGenericMethod = true; + } + else if (method.Parameters.All(parameter => parameter.IsOptional)) + { + anyInvocableMethod = true; + } + } + else if (member is IPropertySymbol property && property.GetMethod?.DeclaredAccessibility == Accessibility.Public) + { + anyReadableProperty = true; + } + } + } + + return anyGenericMethod && !anyInvocableMethod && !anyReadableProperty; + } + + // Mirrors GetValidValuesForParamsSourceAsync: a public all-optional method, else a property with a public + // getter. True only when every candidate of that name has required parameters, bases included. + public static bool SourceResolvesOnlyToRequiredParameterMethod(ITypeSymbol type, string name) + { + bool anyPublicMethod = false; + bool anyAllOptionalMethod = false; + bool anyReadableProperty = false; + + for (ITypeSymbol? current = type; current != null; current = current.BaseType) + { + foreach (var member in current.GetMembers(name)) + { + if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary && method.DeclaredAccessibility == Accessibility.Public) + { + anyPublicMethod = true; + if (method.Parameters.All(parameter => parameter.IsOptional)) + { + anyAllOptionalMethod = true; + } + } + else if (member is IPropertySymbol property && property.GetMethod?.DeclaredAccessibility == Accessibility.Public) + { + anyReadableProperty = true; + } + } + } + + return anyPublicMethod && !anyAllOptionalMethod && !anyReadableProperty; + } + public static INamedTypeSymbol? GetBenchmarkAttributeTypeSymbol(Compilation compilation) => compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.BenchmarkAttribute"); + /// + /// Whether a value of this type can be by-ref-like: one that is, or a type parameter whose constraint admits + /// one. An open declaration is judged on what every substitution guarantees, so a constraint that admits a ref + /// struct is answered like a ref struct. A compiler that cannot express the constraint cannot be given a + /// declaration carrying it either, which is what the older targets fall back to. + /// + public static bool MayBeRefLike(ITypeSymbol type) + { +#if CODE_ANALYSIS_4_12 + if (type is ITypeParameterSymbol { AllowsRefLikeType: true }) + { + return true; + } +#endif + return IsRefLikeType(type); + } + + // ref structs are C# 7.2, but no public symbol carries IsRefLikeType until Roslyn 3.0; the oldest band reads + // the internal property behind it instead. + private static bool IsRefLikeType(ITypeSymbol type) +#if CODE_ANALYSIS_3_0 + => type.IsRefLikeType; +#else + => RefLikeTypePolyfill.IsRefLikeType(type); +#endif + + /// + /// Names which of the two found, so a message reads the same from either source analyzer. + /// + public static string ByRefLikeClause(ITypeSymbol type) + => IsRefLikeType(type) ? "is a ref struct" : "admits a ref struct"; + + /// + /// Which of the two rules applies. A ref struct is one the compiler can see, so the source cannot work; a + /// constraint that merely admits one is decided by the type argument, and one that is not by-ref-like reads + /// perfectly well - so the two are separate ids, configurable apart. + /// + public static DiagnosticDescriptor ByRefLikeRule(ITypeSymbol type) + => IsRefLikeType(type) ? SourceElementMustNotBeByRefLikeRule : SourceElementMayBeByRefLikeRule; + + /// + /// Whether is or derives from it. BenchmarkDotNet resolves + /// its attributes with Type.GetCustomAttributes, which matches derived attribute types, so an analyzer comparing + /// for exact identity would disagree with the runtime about e.g. a user's `MyParamsAttribute : ParamsAttribute`. + /// + public static bool IsOrDerivesFrom(ITypeSymbol? type, INamedTypeSymbol? baseType) + { + if (type == null || baseType == null) + { + return false; + } + for (ITypeSymbol? current = type; current != null; current = current.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(current, baseType)) + { + return true; + } + } + return false; + } + public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTypeSymbol, SyntaxList attributeLists, SemanticModel semanticModel) { if (attributeTypeSymbol == null || attributeTypeSymbol.TypeKind == TypeKind.Error) @@ -33,7 +212,7 @@ public static bool AttributeListsContainAttribute(INamedTypeSymbol? attributeTyp continue; } - if (SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, attributeTypeSymbol)) + if (IsOrDerivesFrom(attributeSyntaxTypeSymbol, attributeTypeSymbol)) { return true; } @@ -53,7 +232,7 @@ public static bool AttributeListContainsAttribute(INamedTypeSymbol? attributeTyp return false; } - return attributeList.Any(ad => SymbolEqualityComparer.Default.Equals(ad.AttributeClass, attributeTypeSymbol)); + return attributeList.Any(ad => IsOrDerivesFrom(ad.AttributeClass, attributeTypeSymbol)); } public static ImmutableArray GetAttributes(string attributeName, Compilation compilation, SyntaxList attributeLists, SemanticModel semanticModel) @@ -78,7 +257,7 @@ public static ImmutableArray GetAttributes(INamedTypeSymbol? at continue; } - if (SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, attributeTypeSymbol)) + if (IsOrDerivesFrom(attributeSyntaxTypeSymbol, attributeTypeSymbol)) { attributesBuilder.Add(attributeSyntax); } @@ -118,6 +297,84 @@ public static Location GetLocation(this AttributeData attributeData) => attributeData.ApplicationSyntaxReference?.SyntaxTree.GetLocation(attributeData.ApplicationSyntaxReference.Span) ?? Location.None; + /// + /// The source-name argument of a [ParamsSource]/[ArgumentsSource] usage - nameof(Values) in + /// [ArgumentsSource(nameof(Values))], or the second argument of the typeof-qualified form. Every rule + /// about the named source reports here, so the squiggle sits on the member to change rather than on the whole + /// attribute. Falls back to the attribute when the argument cannot be located. + /// + public static Location GetSourceNameLocation(this AttributeData attributeData) + { + if (attributeData.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax { ArgumentList: { } argumentList }) + { + // By name first: ConstructorArguments is in parameter order and the syntax list in source order, which + // coincide only while every argument is positional. A named one may sit anywhere. + foreach (var argument in argumentList.Arguments) + { + if (argument.NameColon?.Name.Identifier.ValueText == "name") + { + return argument.Expression.GetLocation(); + } + } + + // All positional, so the name is the last argument - after the type where one is given. + int nameIndex = attributeData.ConstructorArguments.Length == 2 ? 1 : 0; + if (argumentList.Arguments.Count > nameIndex && argumentList.Arguments[nameIndex].NameColon is null) + { + return argumentList.Arguments[nameIndex].Expression.GetLocation(); + } + } + return attributeData.GetLocation(); + } + + /// + /// Finds a [ParamsSource]/[ArgumentsSource] source member (method or property) by name, searching the type + /// and its base types. Mirrors the runtime resolution, which uses GetAllMethods/GetAllProperties (inherited + /// members included), unlike ITypeSymbol.GetMembers which only returns declared members. + /// + public static ISymbol? FindSourceMember(ITypeSymbol type, string name) + { + ISymbol? readableProperty = null; + ISymbol? writeOnlyProperty = null; + ISymbol? otherMethod = null; + + for (ITypeSymbol? current = type; current != null; current = current.BaseType) + { + foreach (var member in current.GetMembers(name)) + { + switch (member) + { + // What the runtime invokes: the first public non-generic method whose parameters are all + // optional. A generic overload is passed over here as it is there, so the rules that read the + // source's return type read the one that will actually be called. + case IMethodSymbol { MethodKind: MethodKind.Ordinary, DeclaredAccessibility: Accessibility.Public, IsGenericMethod: false } method + when method.Parameters.All(parameter => parameter.IsOptional): + return method; + + // The runtime falls back to a property with a public getter, so one of those wins wherever it is + // found - a write-only property nearer the derived end does not hide it, as taking the first of + // either kind would have made it do. + case IPropertySymbol { GetMethod.DeclaredAccessibility: Accessibility.Public }: + readableProperty ??= member; + break; + + // A write-only property is nothing the runtime would read, but it is still the member the name + // most likely meant, so it is returned when nothing better turns up and BDN1305 reports it. + case IPropertySymbol: + writeOnlyProperty ??= member; + break; + + // Nothing the runtime would use, but reporting on it beats reporting nothing. + case IMethodSymbol { MethodKind: MethodKind.Ordinary }: + otherMethod ??= member; + break; + } + } + } + + return readableProperty ?? writeOnlyProperty ?? otherMethod; + } + public static bool IsAssignable(TypedConstant constant, ExpressionSyntax expression, ITypeSymbol targetType, Compilation compilation) { if (constant.IsNull) diff --git a/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md index d26a08b8a5..55ded70c58 100644 --- a/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/BenchmarkDotNet.Analyzers/AnalyzerReleases.Unshipped.md @@ -2,7 +2,17 @@ Rule ID | Category | Severity | Notes ---------|----------|----------|-------------------- -BDN1305 | Usage | Error | [ParamsSource] cannot reference write-only property +BDN1109 | Usage | Error | Required member cannot be set by BenchmarkDotNet +BDN1110 | Usage | Error | Benchmark constructor must not be annotated with [SetsRequiredMembers] +BDN1208 | Usage | Error | [Params(Source\|AllValues)] member name is reserved by code generation +BDN1305 | Usage | Error | [ParamsSource] cannot reference write-only property +BDN1306 | Usage | Error | [ParamsSource] must return a generic enumerable or async enumerable +BDN1307 | Usage | Error | [ParamsSource]/[ArgumentsSource] source method must not have required parameters +BDN1308 | Usage | Error | [ParamsSource]/[ArgumentsSource] source must not have more than one enumerable shape +BDN1310 | Usage | Error | [ParamsSource]/[ArgumentsSource] source method must not be generic +BDN1311 | Usage | Error | [ParamsSource]/[ArgumentsSource] source must not yield a ref struct +BDN1312 | Usage | Warning | [ParamsSource]/[ArgumentsSource] source may yield a ref struct +BDN1504 | Usage | Error | [ArgumentsSource] must return a generic enumerable or async enumerable BDN1600 | Usage | Error | Fields or properties annotated with [BenchmarkCancellation] must be of type CancellationToken BDN1601 | Usage | Error | Fields annotated with [BenchmarkCancellation] must be public BDN1602 | Usage | Error | Properties annotated with [BenchmarkCancellation] must be public @@ -17,4 +27,5 @@ BDN1701 | Usage | Warning | Benchmark/setup/cleanup return type is both awa Rule ID | Category | Severity | Notes ---------|----------|----------|-------------------- -BDN1100 | Usage | Error | Rule removed as GenericTypeArguments now supports abstract classes \ No newline at end of file +BDN1100 | Usage | Error | Rule removed as GenericTypeArguments now supports abstract classes +BDN1206 | Usage | Error | Rule removed as parameters are now assigned through an object initializer, which can set init-only properties \ No newline at end of file diff --git a/src/BenchmarkDotNet.Analyzers/AsyncTypeShapes.cs b/src/BenchmarkDotNet.Analyzers/AsyncTypeShapes.cs index bce65bfab4..741a56a466 100644 --- a/src/BenchmarkDotNet.Analyzers/AsyncTypeShapes.cs +++ b/src/BenchmarkDotNet.Analyzers/AsyncTypeShapes.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis; namespace BenchmarkDotNet.Analyzers; @@ -50,6 +52,87 @@ public static bool IsAsyncEnumerable(ITypeSymbol type, INamedTypeSymbol? asyncEn return false; } + /// + /// Whether a [ParamsSource]/[ArgumentsSource] member's return type offers no usable source shape at all - + /// neither IEnumerable<T> nor IAsyncEnumerable<T>. The non-generic + /// System.Collections.IEnumerable does not qualify on its own: the generated extraction call infers its + /// element type from the source, and a type with no generic instantiation gives inference nothing to bind to. + /// The await-foreach pattern without the IAsyncEnumerable<T> interface is not supported either. + /// + public static bool IsSupportedSourceReturnType(Compilation compilation, ITypeSymbol returnType) + => CountSourceShapes(compilation, returnType) >= 1; + + /// + /// Whether a source's return type offers more than one candidate shape, which BenchmarkDotNet cannot read + /// unambiguously - both an enumerable and an async enumerable, or several instantiations of either one + /// (IEnumerable<int> plus IEnumerable<string>, say). + /// + public static bool IsAmbiguouslyEnumerable(Compilation compilation, ITypeSymbol returnType) + => CountSourceShapes(compilation, returnType) > 1; + + /// + /// The element type a source declares, when its shape is unambiguous - the T of the single + /// IEnumerable<T> or IAsyncEnumerable<T> it offers. That is what the generated + /// extraction call returns, and so what any generated index is applied to. + /// + public static bool TryGetSourceElementType(Compilation compilation, ITypeSymbol returnType, out ITypeSymbol? elementType) + { + elementType = null; + if (CountSourceShapes(compilation, returnType) != 1) + { + return false; + } + + var enumerable = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T); + var asyncEnumerable = compilation.GetTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1"); + + foreach (var candidate in new[] { returnType }.Concat(returnType.AllInterfaces)) + { + if (candidate is INamedTypeSymbol { IsGenericType: true } named + && (SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, enumerable) + || SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, asyncEnumerable))) + { + elementType = named.TypeArguments[0]; + return true; + } + } + return false; + } + + // Type inference needs a *unique* candidate interface, so anything other than exactly one instantiation across + // both shapes fails to compile in the generated code (CS0411) - even when one element type converts to the + // other, as with IEnumerable plus IEnumerable. Counting keeps the two rules disjoint: none + // reports "unsupported shape", several reports "ambiguous shape". + private static int CountSourceShapes(Compilation compilation, ITypeSymbol returnType) + => CountInstantiations(returnType, compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T)) + + CountInstantiations(returnType, compilation.GetTypeByMetadataName("System.Collections.Generic.IAsyncEnumerable`1")); + + /// + /// Counts the distinct closed instantiations of that + /// either is or implements. + /// + private static int CountInstantiations(ITypeSymbol type, INamedTypeSymbol? interfaceDefinition) + { + if (interfaceDefinition == null) + { + return 0; + } + + var found = new HashSet(SymbolEqualityComparer.Default); + if (type is INamedTypeSymbol named && SymbolEqualityComparer.Default.Equals(named.OriginalDefinition, interfaceDefinition)) + { + found.Add(named); + } + foreach (var implemented in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(implemented.OriginalDefinition, interfaceDefinition)) + { + found.Add(implemented); + } + } + return found.Count; + } + /// /// Returns true when exposes a public parameterless GetAwaiter method — /// the necessary precondition for the C# compiler's await binding. The analyzer doesn't drill diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs index aade2e2c84..52621c5769 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ArgumentsAttributeAnalyzer.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; @@ -43,12 +44,27 @@ public class ArgumentsAttributeAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsAttribute_RequiresParameters_Description))); + internal static readonly DiagnosticDescriptor ArgumentsSourceMustReturnEnumerableRule = new( + DiagnosticIds.Attributes_ArgumentsSourceAttribute_MustReturnEnumerable, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Description))); + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] { RequiresBenchmarkAttributeRule, MustHaveMatchingValueCountRule, MustHaveMatchingValueTypeRule, RequiresParametersRule, + ArgumentsSourceMustReturnEnumerableRule, + AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule, + AnalyzerHelper.SourceMethodMustNotBeGenericRule, + AnalyzerHelper.SourceElementMustNotBeByRefLikeRule, + AnalyzerHelper.SourceElementMayBeByRefLikeRule, + AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule, }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) @@ -90,15 +106,15 @@ private static void AnalyzeMethodSymbol(SymbolAnalysisContext context) var argumentsSourceAttributes = new List(); foreach (var attr in methodSymbol.GetAttributes()) { - if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, benchmarkAttributeTypeSymbol)) + if (AnalyzerHelper.IsOrDerivesFrom(attr.AttributeClass, benchmarkAttributeTypeSymbol)) { hasBenchmarkAttribute = true; } - else if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, argumentsAttributeTypeSymbol)) + else if (AnalyzerHelper.IsOrDerivesFrom(attr.AttributeClass, argumentsAttributeTypeSymbol)) { argumentsAttributes.Add(attr); } - else if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, argumentsSourceAttributeTypeSymbol)) + else if (AnalyzerHelper.IsOrDerivesFrom(attr.AttributeClass, argumentsSourceAttributeTypeSymbol)) { argumentsSourceAttributes.Add(attr); } @@ -129,6 +145,14 @@ private static void AnalyzeMethodSymbol(SymbolAnalysisContext context) foreach (var attr in argumentsAttributes) { + // Only [Arguments] itself is guaranteed to carry the values in its own constructor arguments. A derived + // attribute declares whatever constructor it likes and may hand values to base(...), where they are + // invisible here, so its arguments are not the values to inspect. + if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, argumentsAttributeTypeSymbol)) + { + continue; + } + // [Arguments] if (attr.ConstructorArguments.Length == 0) { @@ -173,6 +197,117 @@ private static void AnalyzeMethodSymbol(SymbolAnalysisContext context) } } + foreach (var attr in argumentsSourceAttributes) + { + AnalyzeArgumentsSourceReturnType(attr); + } + + void AnalyzeArgumentsSourceReturnType(AttributeData attr) + { + // These rules need the source's name, which is in this usage's own arguments only when + // [ArgumentsSource] itself was applied - a derived attribute may hand it to base(...), out of sight. + if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, argumentsSourceAttributeTypeSymbol)) + { + return; + } + + // [ArgumentsSource(nameof(Source))] or [ArgumentsSource(typeof(Other), nameof(Other.Source))] + ITypeSymbol? sourceType; + string? sourceName; + if (attr.ConstructorArguments.Length == 1) + { + sourceType = methodSymbol.ContainingType; + sourceName = attr.ConstructorArguments[0].Value as string; + } + else if (attr.ConstructorArguments.Length == 2) + { + sourceType = attr.ConstructorArguments[0].Value as ITypeSymbol; + sourceName = attr.ConstructorArguments[1].Value as string; + } + else + { + return; + } + + if (sourceType == null || string.IsNullOrEmpty(sourceName)) + { + return; + } + + var referencedMember = AnalyzerHelper.FindSourceMember(sourceType, sourceName!); + + if (AnalyzerHelper.SourceResolvesOnlyToGenericMethod(sourceType, sourceName!)) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerHelper.SourceMethodMustNotBeGenericRule, + attr.GetSourceNameLocation(), + sourceName)); + return; + } + + if (AnalyzerHelper.SourceResolvesOnlyToRequiredParameterMethod(sourceType, sourceName!)) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule, + attr.GetSourceNameLocation(), + sourceName)); + return; + } + + ITypeSymbol? returnType = referencedMember switch + { + IMethodSymbol method => method.ReturnType, + IPropertySymbol property => property.Type, + _ => null + }; + + if (returnType == null || returnType.TypeKind == TypeKind.Error) + { + return; + } + + if (AsyncTypeShapes.IsAmbiguouslyEnumerable(context.Compilation, returnType)) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule, + attr.GetSourceNameLocation(), + sourceName, + returnType.ToDisplayString())); + return; + } + + if (!AsyncTypeShapes.IsSupportedSourceReturnType(context.Compilation, returnType)) + { + context.ReportDiagnostic(Diagnostic.Create( + ArgumentsSourceMustReturnEnumerableRule, + attr.GetSourceNameLocation(), + sourceName, + returnType.ToDisplayString())); + return; + } + + if (!AsyncTypeShapes.TryGetSourceElementType(context.Compilation, returnType, out var elementType)) + { + return; + } + + // Discovery reads the values into an object[], which a ref struct cannot enter. Expressible since .NET 10 + // gave IEnumerable an allows-ref-struct type parameter; a ref struct *parameter* is still supported, + // fed from whatever the value is built from. A constraint admitting one is answered the same way, as an + // open declaration is judged on what every substitution guarantees. + if (AnalyzerHelper.MayBeRefLike(elementType!)) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerHelper.ByRefLikeRule(elementType!), + attr.GetSourceNameLocation(), + sourceName, + elementType!.ToDisplayString(), + AnalyzerHelper.ByRefLikeClause(elementType!))); + return; + } + + } + void ReportMustHaveMatchingValueCountDiagnostic(Location diagnosticLocation, int valueCount) => context.ReportDiagnostic(Diagnostic.Create(MustHaveMatchingValueCountRule, diagnosticLocation, diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/BenchmarkCancellationAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/BenchmarkCancellationAttributeAnalyzer.cs index 0bee087118..baa117e26d 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/BenchmarkCancellationAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/BenchmarkCancellationAttributeAnalyzer.cs @@ -97,7 +97,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; if (attributeSyntaxTypeSymbol == null || attributeSyntaxTypeSymbol.TypeKind == TypeKind.Error - || !SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkCancellationAttributeTypeSymbol)) + || !AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, benchmarkCancellationAttributeTypeSymbol)) { return; } diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs index a0cf864918..ec98ae752d 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/GeneralParameterAttributesAnalyzer.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Code; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -71,15 +72,6 @@ public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description))); - internal static readonly DiagnosticDescriptor PropertyCannotBeInitOnlyRule = new( - DiagnosticIds.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly, - AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title)), - AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat)), - "Usage", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description))); - internal static readonly DiagnosticDescriptor ParamsSourceCannotUseWriteOnlyPropertyRule = new( DiagnosticIds.Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty, AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Title)), @@ -89,6 +81,24 @@ public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer isEnabledByDefault: true, description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Description))); + internal static readonly DiagnosticDescriptor ParamsSourceMustReturnEnumerableRule = new( + DiagnosticIds.Attributes_ParamsSourceAttribute_MustReturnEnumerable, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_MustReturnEnumerable_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_MustReturnEnumerable_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.Attributes_ParamsSourceAttribute_MustReturnEnumerable_Description))); + + internal static readonly DiagnosticDescriptor ReservedMemberNameRule = new( + DiagnosticIds.General_ReservedMemberName, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_ReservedMemberName_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_ReservedMemberName_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_ReservedMemberName_Description))); + public override ImmutableArray SupportedDiagnostics => new DiagnosticDescriptor[] { MutuallyExclusiveOnFieldRule, @@ -97,9 +107,15 @@ public class GeneralParameterAttributesAnalyzer : DiagnosticAnalyzer PropertyMustBePublic, NotValidOnReadonlyFieldRule, NotValidOnConstantFieldRule, - PropertyCannotBeInitOnlyRule, PropertyMustHavePublicSetterRule, ParamsSourceCannotUseWriteOnlyPropertyRule, + ParamsSourceMustReturnEnumerableRule, + ReservedMemberNameRule, + AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule, + AnalyzerHelper.SourceMethodMustNotBeGenericRule, + AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule, + AnalyzerHelper.SourceElementMustNotBeByRefLikeRule, + AnalyzerHelper.SourceElementMayBeByRefLikeRule, }.ToImmutableArray(); public override void Initialize(AnalysisContext analysisContext) @@ -136,9 +152,9 @@ private static void Analyze(SyntaxNodeAnalysisContext context) if (attributeSyntaxTypeSymbol == null || attributeSyntaxTypeSymbol.TypeKind == TypeKind.Error || - (!SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, paramsAttributeTypeSymbol) - && !SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, paramsSourceAttributeTypeSymbol) - && !SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, paramsAllValuesAttributeTypeSymbol))) + (!AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, paramsAttributeTypeSymbol) + && !AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, paramsSourceAttributeTypeSymbol) + && !AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, paramsAllValuesAttributeTypeSymbol))) { return; } @@ -154,9 +170,12 @@ private static void Analyze(SyntaxNodeAnalysisContext context) Location? fieldConstModifierLocation = null; Location? fieldReadonlyModifierLocation = null; string fieldOrPropertyIdentifier; - Location? propertyInitAccessorKeywordLocation = null; Location fieldOrPropertyIdentifierLocation; + // One field declaration can declare several names - `[Params] public int a, b;` applies the attribute to + // both - and each is a member the runnable's object initializer has to bind. + ImmutableArray<(string Name, Location Location)> declaredNames; bool propertyIsMissingAssignableSetter = false; + bool fieldOrPropertyIsStatic; DiagnosticDescriptor fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule; DiagnosticDescriptor fieldOrPropertyMustBePublicDiagnosticRule; @@ -164,6 +183,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) { declaredAttributes = fieldDeclarationSyntax.AttributeLists.SelectMany(als => als.Attributes).ToImmutableArray(); fieldOrPropertyIsPublic = fieldDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword); + fieldOrPropertyIsStatic = fieldDeclarationSyntax.Modifiers.Any(SyntaxKind.StaticKeyword); var fieldConstModifierIndex = fieldDeclarationSyntax.Modifiers.IndexOf(SyntaxKind.ConstKeyword); fieldConstModifierLocation = fieldConstModifierIndex >= 0 ? fieldDeclarationSyntax.Modifiers[fieldConstModifierIndex].GetLocation() : null; @@ -173,6 +193,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context) fieldOrPropertyIdentifier = fieldDeclarationSyntax.Declaration.Variables[0].Identifier.ToString(); fieldOrPropertyIdentifierLocation = fieldDeclarationSyntax.Declaration.Variables[0].Identifier.GetLocation(); + declaredNames = ImmutableArray.CreateRange(fieldDeclarationSyntax.Declaration.Variables + .Select(variable => (variable.Identifier.ToString(), variable.Identifier.GetLocation()))); fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule = MutuallyExclusiveOnFieldRule; fieldOrPropertyMustBePublicDiagnosticRule = FieldMustBePublic; } @@ -180,17 +202,19 @@ private static void Analyze(SyntaxNodeAnalysisContext context) { declaredAttributes = propertyDeclarationSyntax.AttributeLists.SelectMany(als => als.Attributes).ToImmutableArray(); fieldOrPropertyIsPublic = propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.PublicKeyword); + fieldOrPropertyIsStatic = propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.StaticKeyword); fieldOrPropertyIdentifier = propertyDeclarationSyntax.Identifier.ToString(); + // `init` counts: the runnable assigns parameters through an object initializer. + var propertyAccessors = propertyDeclarationSyntax.AccessorList?.Accessors; + propertyIsMissingAssignableSetter = !HasAssignableAccessor(propertyAccessors, SyntaxKind.SetAccessorDeclaration) #if CODE_ANALYSIS_3_8 - var propertyInitAccessorIndex = propertyDeclarationSyntax.AccessorList?.Accessors.IndexOf(SyntaxKind.InitAccessorDeclaration); - propertyInitAccessorKeywordLocation = propertyInitAccessorIndex >= 0 ? propertyDeclarationSyntax.AccessorList!.Accessors[propertyInitAccessorIndex.Value].Keyword.GetLocation() : null; + && !HasAssignableAccessor(propertyAccessors, SyntaxKind.InitAccessorDeclaration) #endif - - var propertySetAccessorIndex = propertyDeclarationSyntax.AccessorList?.Accessors.IndexOf(SyntaxKind.SetAccessorDeclaration); - propertyIsMissingAssignableSetter = !propertySetAccessorIndex.HasValue || propertySetAccessorIndex.Value < 0 || propertyDeclarationSyntax.AccessorList!.Accessors[propertySetAccessorIndex.Value].Modifiers.Any(); + ; fieldOrPropertyIdentifierLocation = propertyDeclarationSyntax.Identifier.GetLocation(); + declaredNames = ImmutableArray.Create((fieldOrPropertyIdentifier, fieldOrPropertyIdentifierLocation)); fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule = MutuallyExclusiveOnPropertyRule; fieldOrPropertyMustBePublicDiagnosticRule = PropertyMustBePublic; } @@ -209,9 +233,10 @@ private static void Analyze(SyntaxNodeAnalysisContext context) fieldConstModifierLocation, fieldReadonlyModifierLocation, fieldOrPropertyIdentifier, - propertyInitAccessorKeywordLocation, propertyIsMissingAssignableSetter, + fieldOrPropertyIsStatic, fieldOrPropertyIdentifierLocation, + declaredNames, fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule, fieldOrPropertyMustBePublicDiagnosticRule, attributeSyntax, @@ -228,21 +253,24 @@ private static void AnalyzeFieldOrPropertySymbol( Location? fieldConstModifierLocation, Location? fieldReadonlyModifierLocation, string fieldOrPropertyIdentifier, - Location? propertyInitAccessorKeywordLocation, bool propertyIsMissingAssignableSetter, + bool fieldOrPropertyIsStatic, Location fieldOrPropertyIdentifierLocation, + ImmutableArray<(string Name, Location Location)> declaredNames, DiagnosticDescriptor fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule, DiagnosticDescriptor fieldOrPropertyMustBePublicDiagnosticRule, AttributeSyntax attributeSyntax, SyntaxNode attributeTarget) { - ImmutableArray applicableParameterAttributeTypeSymbols = - [ + ImmutableArray applicableParameterAttributeTypeSymbols = ImmutableArray.Create( paramsAttributeTypeSymbol, paramsSourceAttributeTypeSymbol, - paramsAllValuesAttributeTypeSymbol - ]; + paramsAllValuesAttributeTypeSymbol); + // Counted by the attribute *class* written on the member, not by the parameter attribute it maps onto: a + // derived attribute maps to the base it derives from, so two different classes deriving from the same one + // are still two usages, matching how the runtime resolves them. + var declaredParameterAttributeTypeSymbols = new HashSet(SymbolEqualityComparer.Default); var parameterAttributeTypeSymbols = new HashSet(SymbolEqualityComparer.Default); foreach (var declaredAttributeSyntax in declaredAttributes) @@ -252,23 +280,33 @@ private static void AnalyzeFieldOrPropertySymbol( { foreach (var applicableParameterAttributeTypeSymbol in applicableParameterAttributeTypeSymbols) { - if (SymbolEqualityComparer.Default.Equals(declaredAttributeTypeSymbol, applicableParameterAttributeTypeSymbol)) + if (AnalyzerHelper.IsOrDerivesFrom(declaredAttributeTypeSymbol, applicableParameterAttributeTypeSymbol)) { - if (!parameterAttributeTypeSymbols.Add(applicableParameterAttributeTypeSymbol)) + // Only the very same class applied twice is CS0579's to report, and repeating it here + // would say it twice. Two different classes in one family are not CS0579 - the compiler + // is silent about them - so they must reach the duplicate rule below instead of ending + // the analysis and taking every other diagnostic for this member with them. + if (!declaredParameterAttributeTypeSymbols.Add(declaredAttributeTypeSymbol)) { return; } + + parameterAttributeTypeSymbols.Add(applicableParameterAttributeTypeSymbol); + + // At most one family per attribute; the three are siblings today, and breaking keeps the + // count right if one ever derives from another. + break; } } } } - if (parameterAttributeTypeSymbols.Count == 0) + if (declaredParameterAttributeTypeSymbols.Count == 0) { return; } - if (parameterAttributeTypeSymbols.Count != 1) + if (declaredParameterAttributeTypeSymbols.Count != 1) { context.ReportDiagnostic(Diagnostic.Create(fieldOrPropertyCannotHaveMoreThanOneParameterAttributeAppliedDiagnosticRule, attributeSyntax.GetLocation(), @@ -278,6 +316,27 @@ private static void AnalyzeFieldOrPropertySymbol( return; } + // The runnable derives from the benchmark type and assigns each instance parameter member through an object + // initializer, which binds the member name unqualified. A parameter member named like a generated member (all + // __-prefixed) therefore binds to the generated member and fails to compile. (Static parameters, sources, + // arguments, and non-parameter members are reached via type-qualification/`base`/hiding and don't collide, so + // only instance parameter members are checked.) + // Every declared name, not only the first: the runtime reports each parameter member it cannot assign. + if (!fieldOrPropertyIsStatic) + { + foreach (var (name, location) in declaredNames) + { + if (RunnableConstants.ReservedInstanceMemberNames.Contains(name)) + { + context.ReportDiagnostic(Diagnostic.Create(ReservedMemberNameRule, + location, + name, + attributeSyntax.Name.ToString()) + ); + } + } + } + if (fieldConstModifierLocation != null) { context.ReportDiagnostic(Diagnostic.Create(NotValidOnConstantFieldRule, @@ -306,15 +365,7 @@ private static void AnalyzeFieldOrPropertySymbol( ); } - if (propertyInitAccessorKeywordLocation != null) - { - context.ReportDiagnostic(Diagnostic.Create(PropertyCannotBeInitOnlyRule, - propertyInitAccessorKeywordLocation, - fieldOrPropertyIdentifier, - attributeSyntax.Name.ToString()) - ); - } - else if (propertyIsMissingAssignableSetter) + if (propertyIsMissingAssignableSetter) { context.ReportDiagnostic(Diagnostic.Create(PropertyMustHavePublicSetterRule, fieldOrPropertyIdentifierLocation, @@ -354,6 +405,15 @@ private static void AnalyzeParamsSourceWriteOnlyProperty( return; } + // These rules need the source's name, and the name is only in this usage's own constructor arguments when + // [ParamsSource] itself was applied. A derived attribute may hand it to base(...), where the runtime still + // reads it off the Name property but nothing here can see it - guessing from whatever arguments the derived + // constructor happens to take would resolve some other member, or none. + if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.ParamsSourceAttribute"))) + { + return; + } + string? sourceName = null; ITypeSymbol? targetType = null; @@ -404,30 +464,89 @@ private static void AnalyzeParamsSourceWriteOnlyProperty( return; } - var referencedMember = targetType.GetMembers(sourceName!).FirstOrDefault(); + var referencedMember = AnalyzerHelper.FindSourceMember(targetType, sourceName!); + + Location location = attributeData.GetSourceNameLocation(); + if (referencedMember is IPropertySymbol propertySymbol && propertySymbol.SetMethod != null && propertySymbol.GetMethod == null) { - Location? location = null; - if (attributeSyntax.ArgumentList != null) - { - if (attributeData.ConstructorArguments.Length == 1 && attributeSyntax.ArgumentList.Arguments.Count > 0) - { - location = attributeSyntax.ArgumentList.Arguments[0].Expression.GetLocation(); - } - else if (attributeData.ConstructorArguments.Length == 2 && attributeSyntax.ArgumentList.Arguments.Count > 1) - { - location = attributeSyntax.ArgumentList.Arguments[1].Expression.GetLocation(); - } - } - location ??= attributeSyntax.GetLocation(); - context.ReportDiagnostic(Diagnostic.Create( ParamsSourceCannotUseWriteOnlyPropertyRule, location, sourceName)); + return; + } + + if (AnalyzerHelper.SourceResolvesOnlyToGenericMethod(targetType, sourceName!)) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerHelper.SourceMethodMustNotBeGenericRule, + location, + sourceName)); + return; + } + + if (AnalyzerHelper.SourceResolvesOnlyToRequiredParameterMethod(targetType, sourceName!)) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule, + location, + sourceName)); + return; + } + + ITypeSymbol? returnType = referencedMember switch + { + IMethodSymbol method => method.ReturnType, + IPropertySymbol property => property.Type, + _ => null + }; + + if (returnType == null || returnType.TypeKind == TypeKind.Error) + { + return; + } + + if (AsyncTypeShapes.IsAmbiguouslyEnumerable(context.Compilation, returnType)) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule, + location, + sourceName, + returnType.ToDisplayString())); + } + else if (!AsyncTypeShapes.IsSupportedSourceReturnType(context.Compilation, returnType)) + { + context.ReportDiagnostic(Diagnostic.Create( + ParamsSourceMustReturnEnumerableRule, + location, + sourceName, + returnType.ToDisplayString())); + } + else if (AsyncTypeShapes.TryGetSourceElementType(context.Compilation, returnType, out var elementType) + && AnalyzerHelper.MayBeRefLike(elementType!)) + { + context.ReportDiagnostic(Diagnostic.Create( + AnalyzerHelper.ByRefLikeRule(elementType!), + location, + sourceName, + elementType!.ToDisplayString(), + AnalyzerHelper.ByRefLikeClause(elementType!))); + } + } + + // An accessor that an object initializer can assign through: `set` or `init`, without an accessibility + // modifier (a non-public one can't be reached from the generated runnable). + private static bool HasAssignableAccessor(SyntaxList? accessors, SyntaxKind kind) + { + if (accessors is not { } list) + { + return false; } + int index = list.IndexOf(kind); + return index >= 0 && !list[index].Modifiers.Any(); } private static string? ExtractNameFromExpression(ExpressionSyntax expression, SemanticModel semanticModel) diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs index 7c6ed1edd7..d20e54d6b7 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAllValuesAttributeAnalyzer.cs @@ -59,7 +59,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var paramsAllValuesAttributeTypeSymbol = GetParamsAllValuesAttributeTypeSymbol(context.Compilation); var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; - if (!SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, paramsAllValuesAttributeTypeSymbol)) + if (!AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, paramsAllValuesAttributeTypeSymbol)) { return; } diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs index b1de24f56c..4d252f4f5c 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/ParamsAttributeAnalyzer.cs @@ -73,7 +73,7 @@ private void Analyze(SymbolAnalysisContext context) var paramsAttributeTypeSymbol = GetParamsAttributeTypeSymbol(context.Compilation); var attrs = context.Symbol.GetAttributes(); - var paramsAttributes = attrs.Where(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, paramsAttributeTypeSymbol)).ToImmutableArray(); + var paramsAttributes = attrs.Where(attr => AnalyzerHelper.IsOrDerivesFrom(attr.AttributeClass, paramsAttributeTypeSymbol)).ToImmutableArray(); if (paramsAttributes.Length != 1) { // Don't analyze zero or multiple [Params] (multiple is not legal and already handled by GeneralParameterAttributesAnalyzer). @@ -82,6 +82,14 @@ private void Analyze(SymbolAnalysisContext context) var attr = paramsAttributes[0]; + // Only [Params] itself is guaranteed to carry the values in its own constructor arguments. A derived + // attribute declares whatever constructor it likes and may hand values to base(...) - `BoolParams() : + // base(true, false)` - where they are invisible here, so its arguments are not the values to inspect. + if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, paramsAttributeTypeSymbol)) + { + return; + } + // [Params] if (attr.ConstructorArguments.Length == 0) { diff --git a/src/BenchmarkDotNet.Analyzers/Attributes/SetupCleanupAsyncEnumerableAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/Attributes/SetupCleanupAsyncEnumerableAnalyzer.cs index 08102e7201..8303c2bf7e 100644 --- a/src/BenchmarkDotNet.Analyzers/Attributes/SetupCleanupAsyncEnumerableAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/Attributes/SetupCleanupAsyncEnumerableAnalyzer.cs @@ -91,7 +91,7 @@ private static void AnalyzeMethod( { foreach (var candidate in captured.AttributeSymbols) { - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, candidate)) + if (AnalyzerHelper.IsOrDerivesFrom(attributeData.AttributeClass, candidate)) { matchedAttribute = candidate; break; diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj index 66d475e342..40beb5a5d8 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNet.Analyzers.csproj @@ -1,40 +1,28 @@  - netstandard2.0 - false BenchmarkDotNet.Analyzers - true - $(NoWarn);CS1591 - - 5.0 - bin\$(Configuration)\roslyn$(MccVersion)\cs - false - $(DefineConstants);CODE_ANALYSIS_3_8 - $(DefineConstants);CODE_ANALYSIS_4_8 - $(DefineConstants);CODE_ANALYSIS_5_0 + + + $(NoWarn);RS2007 $(NoWarn);RS2002 - enable - - - - - - - - - - - + + + + + + + True @@ -52,4 +40,4 @@ - + \ No newline at end of file diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs index cbbf83fa08..70f5d566fe 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.Designer.cs @@ -1,1084 +1,1237 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace BenchmarkDotNet.Analyzers { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class BenchmarkDotNetAnalyzerResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal BenchmarkDotNetAnalyzerResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BenchmarkDotNet.Analyzers.BenchmarkDotNetAnalyzerResources", typeof(BenchmarkDotNetAnalyzerResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method. - /// - internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Description { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Expected {0} value{1} as declared by the benchmark method '{2}', but found {3}. Update the attribute usage or method to match.. - /// - internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method. - /// - internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Title { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The values passed to an [Arguments] attribute must match the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order. - /// - internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unexpected type for argument value '{0}'. Expected '{1}' but found '{2}'.. - /// - internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Values passed to an [Arguments] attribute must match exactly the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order. - /// - internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Title { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [Arguments(Source)] attribute can only be used on methods annotated with the [Benchmark] attribute. - /// - internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [Arguments(Source)] attribute can only be used on methods annotated with the [Benchmark] attribute. - /// - internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_Title { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The values passed to an [Arguments] must have parameter(s). - /// - internal static string Attributes_ArgumentsAttribute_RequiresParameters_Description { - get { - return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresParameters_Description", resourceCulture); - } - } - - /// - /// 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.. - /// - internal static string Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_Description { - get { - return ResourceManager.GetString("Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_D" + - "escription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark method '{0}' without an [ArgumentsSource] or [Arguments] attribute(s) cannot declare parameters. - /// - internal static string Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_M" + - "essageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark methods without an [ArgumentsSource] or [Arguments] attribute(s) cannot declare parameters. - /// - internal static string Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_Title { - get { - return ResourceManager.GetString("Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_T" + - "itle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A field annotated with a parameter attribute must be public. - /// - internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_Description { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Field '{0}' annotated with [{1}] must be public. - /// - internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Fields annotated with a parameter attribute must be public. - /// - internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_Title { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a field at any one time. - /// - internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Description { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Duplicate parameter attribute on field '{0}'. - /// - internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only one parameter attribute can be applied to a field. - /// - internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Title { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a property at any one time. - /// - internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Description { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Duplicate parameter attribute on property '{0}'. - /// - internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only one parameter attribute can be applied to a property. - /// - internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Title { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parameter attribute [{0}] is not valid on constants. It is only valid on non-constant field declarations.. - /// - internal static string Attributes_GeneralParameterAttributes_NotValidOnConstantField_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnConstantField_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parameter attributes are not valid on constant field declarations. - /// - internal static string Attributes_GeneralParameterAttributes_NotValidOnConstantField_Title { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnConstantField_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Parameter attributes are not valid on fields with a readonly modifier. - /// - internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Description { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Modifier 'readonly' is not valid on field '{0}' annotated with parameter attribute [{1}]. - /// - internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Fields annotated with a parameter attribute cannot be read-only. - /// - internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Title { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A property annotated with a parameter attribute must have a public, assignable setter i.e. { set; }. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Property '{0}' annotated with [{1}] cannot be init-only. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Properties annotated with a parameter attribute cannot have an init-only setter. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A property annotated with a parameter attribute must be public. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_Description { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must be public. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Properties annotated with a parameter attribute must be public. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_Title { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A property annotated with a parameter attribute must have a public setter; make sure that the access modifier of the setter is empty and that the property is not an auto-property or an expression-bodied property.. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must have a public setter. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_MessageFormat { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Properties annotated with a parameter attribute must have a public setter. - /// - internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Title { - get { - return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [ParamsAllValues] attribute cannot be applied to a field or property of an enum type marked with the [Flags] attribute. Use this attribute only with non-flags enum types, as [Flags] enums support bitwise combinations that cannot be exhaustively enumerated.. - /// - internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Description { - get { - return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Desc" + - "ription", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Field or property enum type '{0}' is marked with [Flags] and cannot be used with this attribute. - /// - internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Mess" + - "ageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [ParamsAllValues] attribute cannot be applied to fields or properties of enum types marked with [Flags]. - /// - internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Title { - get { - return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Titl" + - "e", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [ParamsAllValues] attribute can only be applied to a field or property of enum or bool type (or nullable of these types). - /// - internal static string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_MessageFo" + - "rmat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [ParamsAllValues] attribute is only valid on fields or properties of enum or bool type and nullable type for another allowed type. - /// - internal static string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_Title { - get { - return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The type of each value provided to the [Params] attribute must match the type of (or be implicitly convertible to) the field or property it is applied to. - /// - internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_Description { - get { - return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unexpected type for parameter value '{0}'. Expected '{1}' but found '{2}'.. - /// - internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Type of all value(s) passed to the [Params] attribute must match the type of (or be implicitly convertible to) the annotated field or property. - /// - internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_Title { - get { - return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [Params] attribute requires at least one value. No values were provided, or an empty array was specified.. - /// - internal static string Attributes_ParamsAttribute_MustHaveValues_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveValues_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [Params] attribute must include at least one value. - /// - internal static string Attributes_ParamsAttribute_MustHaveValues_Title { - get { - return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveValues_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Providing a single value to the [Params] attribute is unnecessary. This attribute is only useful when provided two or more values.. - /// - internal static string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Unnecessary single value passed to [Params] attribute. - /// - internal static string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_Title { - get { - return ResourceManager.GetString("Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ParamsSource references a write-only property (a property with only a setter and no getter). Write-only properties cannot be read at runtime and will cause a NullReferenceException. Use a property with a getter or a method instead. - /// - internal static string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Description { - get { - return ResourceManager.GetString("Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ParamsSource cannot reference write-only property '{0}'. Write-only properties cannot be read and will cause a runtime error. - /// - internal static string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_MessageFormat { - get { - return ResourceManager.GetString("Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ParamsSource cannot reference a write-only property. - /// - internal static string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Title { - get { - return ResourceManager.GetString("Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A generic benchmark class referenced in the BenchmarkRunner.Run method must be annotated with at least one [GenericTypeArguments] attribute. - /// - internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Description { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + - "ntsAttribute_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Referenced generic benchmark class '{0}' has no [GenericTypeArguments] attribute(s). - /// - internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_MessageFormat { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + - "ntsAttribute_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Generic benchmark classes must be annotated with at least one [GenericTypeArguments] attribute. - /// - internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Title { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + - "ntsAttribute_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The referenced benchmark class (or any of its inherited classes) must have at least one method annotated with the [Benchmark] attribute. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Description { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Intended benchmark class '{0}' (or any of its ancestors) has no method(s) annotated with the [Benchmark] attribute. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_MessageFormat { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark class (or any of its ancestors) has no annotated method(s). - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Title { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A benchmark class referenced in the BenchmarkRunner.Run method must be non-abstract. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Description { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Referenced benchmark class '{0}' cannot be abstract. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_MessageFormat { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark classes must be non-abstract. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Title { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Referenced benchmark class '{0}' must be public. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMustBePublic_MessageFormat { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBePublic_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark classes must be public. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMustBePublic_Title { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBePublic_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A benchmark class referenced in the BenchmarkRunner.Run method must be unsealed. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Description { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Referenced benchmark class '{0}' is sealed. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_MessageFormat { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark classes must be unsealed. - /// - internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Title { - get { - return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A benchmark class must be an instance class. - /// - internal static string General_BenchmarkClass_ClassMustBeNonStatic_Description { - get { - return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark class '{0}' cannot be static. - /// - internal static string General_BenchmarkClass_ClassMustBeNonStatic_MessageFormat { - get { - return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark classes must be non-static. - /// - internal static string General_BenchmarkClass_ClassMustBeNonStatic_Title { - get { - return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A benchmark class annotated with a [GenericTypeArguments] attribute must be generic, having between one to three type parameters. - /// - internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Description { - get { - return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Descri" + - "ption", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Attribute [GenericTypeArguments] can only be applied to a generic class. - /// - internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_MessageFormat { - get { - return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Messag" + - "eFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark classes annotated with a [GenericTypeArguments] attribute must be generic. - /// - internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Title { - get { - return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class. - /// - internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_Description { - get { - return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + - "Count_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Expected {0} type argument{1} as declared on the benchmark class '{2}', but found {3}. Update the attribute usage or the type parameter list of the class declaration to match.. - /// - internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_MessageFormat { - get { - return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + - "Count_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class. - /// - internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_Title { - get { - return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + - "Count_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A method annotated with the [Benchmark] attribute must be non-generic. - /// - internal static string General_BenchmarkClass_MethodMustBeNonGeneric_Description { - get { - return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The benchmark method '{0}' must be non-generic. - /// - internal static string General_BenchmarkClass_MethodMustBeNonGeneric_MessageFormat { - get { - return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark methods must be non-generic. - /// - internal static string General_BenchmarkClass_MethodMustBeNonGeneric_Title { - get { - return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A method annotated with the [Benchmark] attribute must be public. - /// - internal static string General_BenchmarkClass_MethodMustBePublic_Description { - get { - return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The benchmark method '{0}' must be public. - /// - internal static string General_BenchmarkClass_MethodMustBePublic_MessageFormat { - get { - return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Benchmark methods must be public. - /// - internal static string General_BenchmarkClass_MethodMustBePublic_Title { - get { - return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only one benchmark method can be marked as baseline per class. - /// - internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaseline_MessageFormat { - get { - return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaseline_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only one benchmark method can be baseline per class. - /// - internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaseline_Title { - get { - return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaseline_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only one benchmark method can be marked as baseline per class and category. - /// - internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_MessageFormat { - get { - return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Only one benchmark method can be baseline per class and category. - /// - internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_Title { - get { - return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Passing a single null argument creates a null params array. Use multiple arguments (e.g., null, "SomeCategory", ...) or a non-null value instead.. - /// - internal static string General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_MessageFormat { - get { - return ResourceManager.GetString("General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_M" + - "essageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Single null argument to the [BenchmarkCategory] attribute results in unintended null array. - /// - internal static string General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_Title { - get { - return ResourceManager.GetString("General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_T" + - "itle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Fields or properties annotated with [BenchmarkCancellation] must be of type CancellationToken. - /// - internal static string Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_Title { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Field or property '{0}' annotated with [BenchmarkCancellation] is of type '{1}' but must be of type 'System.Threading.CancellationToken'. - /// - internal static string Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_MessageFormat { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_MessageFor" + - "mat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [BenchmarkCancellation] attribute can only be applied to fields or properties of type System.Threading.CancellationToken. - /// - internal static string Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_Description { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_Description" + - "", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Fields annotated with [BenchmarkCancellation] must be public. - /// - internal static string Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_Title { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Field '{0}' annotated with [{1}] must be public. - /// - internal static string Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_MessageFormat { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [BenchmarkCancellation] attribute requires the field to be public so BenchmarkDotNet can inject the cancellation token. - /// - internal static string Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_Description { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Properties annotated with [BenchmarkCancellation] must be public. - /// - internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_Title { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must be public. - /// - internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_MessageFormat { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [BenchmarkCancellation] attribute requires the property to be public so BenchmarkDotNet can inject the cancellation token. - /// - internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_Description { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [BenchmarkCancellation] attribute is not valid on readonly fields. - /// - internal static string Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_Title { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Field '{0}' annotated with [{1}] cannot be readonly. - /// - internal static string Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_MessageFormat { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [BenchmarkCancellation] attribute cannot be applied to readonly fields because BenchmarkDotNet needs to set the field value at runtime. - /// - internal static string Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_Description { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Properties annotated with [BenchmarkCancellation] must have a public setter. - /// - internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_Title { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must have a public setter. - /// - internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_MessageFormat { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_MessageFor" + - "mat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The [BenchmarkCancellation] attribute requires a public setter so BenchmarkDotNet can inject the cancellation token. Init-only setters are supported.. - /// - internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_Description { - get { - return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_Descriptio" + - "n", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Async benchmarks should have a [BenchmarkCancellation] property for cancellation support. - /// - internal static string General_AsyncBenchmark_ShouldHaveCancellationToken_Title { - get { - return ResourceManager.GetString("General_AsyncBenchmark_ShouldHaveCancellationToken_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Class '{0}' contains async benchmark methods but no [BenchmarkCancellation] property. Consider adding a public CancellationToken property with [BenchmarkCancellation] attribute to support benchmark cancellation.. - /// - internal static string General_AsyncBenchmark_ShouldHaveCancellationToken_MessageFormat { - get { - return ResourceManager.GetString("General_AsyncBenchmark_ShouldHaveCancellationToken_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Async benchmark methods should check for cancellation to allow graceful termination. Add a public property 'public CancellationToken CancellationToken { get; set; }' annotated with [BenchmarkCancellation] to enable this functionality.. - /// - internal static string General_AsyncBenchmark_ShouldHaveCancellationToken_Description { - get { - return ResourceManager.GetString("General_AsyncBenchmark_ShouldHaveCancellationToken_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Setup and cleanup methods must not return an async enumerable. - /// - internal static string Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_Title { - get { - return ResourceManager.GetString("Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [{0}] method '{1}' returns an async enumerable, which is not supported. - /// - internal static string Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_MessageFormat { - get { - return ResourceManager.GetString("Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BenchmarkDotNet awaits awaitable return values of [GlobalSetup], [GlobalCleanup], [IterationSetup], and [IterationCleanup] methods, but it does not enumerate async enumerables — the iterator body would silently never run. Change the return type to void, Task, ValueTask, or another awaitable.. - /// - internal static string Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_Description { - get { - return ResourceManager.GetString("Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_Description", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Return type is both awaitable and an async enumerable. - /// - internal static string General_AwaitableAsyncEnumerable_AmbiguousReturnType_Title { - get { - return ResourceManager.GetString("General_AwaitableAsyncEnumerable_AmbiguousReturnType_Title", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to [{0}] method '{1}' returns '{2}', which is both awaitable and an async enumerable; BenchmarkDotNet awaits the value and never enumerates it. - /// - internal static string General_AwaitableAsyncEnumerable_AmbiguousReturnType_MessageFormat { - get { - return ResourceManager.GetString("General_AwaitableAsyncEnumerable_AmbiguousReturnType_MessageFormat", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to When a benchmark or setup/cleanup return type satisfies both the awaitable pattern (a public GetAwaiter) and the async-enumerable pattern (a public GetAsyncEnumerator), BenchmarkDotNet treats it as awaitable — the iterator's body is never executed. Pick one shape so the intent is unambiguous: drop GetAwaiter if you want it consumed as an async enumerable, or drop GetAsyncEnumerator if you want it awaited.. - /// - internal static string General_AwaitableAsyncEnumerable_AmbiguousReturnType_Description { - get { - return ResourceManager.GetString("General_AwaitableAsyncEnumerable_AmbiguousReturnType_Description", resourceCulture); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BenchmarkDotNet.Analyzers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class BenchmarkDotNetAnalyzerResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal BenchmarkDotNetAnalyzerResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BenchmarkDotNet.Analyzers.BenchmarkDotNetAnalyzerResources", typeof(BenchmarkDotNetAnalyzerResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Description { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expected {0} value{1} as declared by the benchmark method '{2}', but found {3}. Update the attribute usage or method to match.. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueCount_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The values passed to an [Arguments] attribute must match the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected type for argument value '{0}'. Expected '{1}' but found '{2}'.. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Values passed to an [Arguments] attribute must match exactly the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order. + /// + internal static string Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_MustHaveMatchingValueType_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [Arguments(Source)] attribute can only be used on methods annotated with the [Benchmark] attribute. + /// + internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [Arguments(Source)] attribute can only be used on methods annotated with the [Benchmark] attribute. + /// + internal static string Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresBenchmarkAttribute_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The values passed to an [Arguments] must have parameter(s). + /// + internal static string Attributes_ArgumentsAttribute_RequiresParameters_Description { + get { + return ResourceManager.GetString("Attributes_ArgumentsAttribute_RequiresParameters_Description", resourceCulture); + } + } + + /// + /// 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.. + /// + internal static string Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_Description { + get { + return ResourceManager.GetString("Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_D" + + "escription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark method '{0}' without an [ArgumentsSource] or [Arguments] attribute(s) cannot declare parameters. + /// + internal static string Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_M" + + "essageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark methods without an [ArgumentsSource] or [Arguments] attribute(s) cannot declare parameters. + /// + internal static string Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_Title { + get { + return ResourceManager.GetString("Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters_T" + + "itle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A field annotated with a parameter attribute must be public. + /// + internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field '{0}' annotated with [{1}] must be public. + /// + internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fields annotated with a parameter attribute must be public. + /// + internal static string Attributes_GeneralParameterAttributes_FieldMustBePublic_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_FieldMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a field at any one time. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Duplicate parameter attribute on field '{0}'. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one parameter attribute can be applied to a field. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a property at any one time. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Duplicate parameter attribute on property '{0}'. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one parameter attribute can be applied to a property. + /// + internal static string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attribute [{0}] is not valid on constants. It is only valid on non-constant field declarations.. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnConstantField_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnConstantField_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attributes are not valid on constant field declarations. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnConstantField_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnConstantField_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter attributes are not valid on fields with a readonly modifier. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Modifier 'readonly' is not valid on field '{0}' annotated with parameter attribute [{1}]. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fields annotated with a parameter attribute cannot be read-only. + /// + internal static string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_NotValidOnReadonlyField_Title", resourceCulture); + } + } + +/// + /// Looks up a localized string similar to A property annotated with a parameter attribute must be public. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must be public. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties annotated with a parameter attribute must be public. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustBePublic_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A property annotated with a parameter attribute must have a public setter; make sure that the access modifier of the setter is empty and that the property is not an auto-property or an expression-bodied property.. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must have a public setter. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_MessageFormat { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties annotated with a parameter attribute must have a public setter. + /// + internal static string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Title { + get { + return ResourceManager.GetString("Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [ParamsAllValues] attribute cannot be applied to a field or property of an enum type marked with the [Flags] attribute. Use this attribute only with non-flags enum types, as [Flags] enums support bitwise combinations that cannot be exhaustively enumerated.. + /// + internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Description { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Desc" + + "ription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field or property enum type '{0}' is marked with [Flags] and cannot be used with this attribute. + /// + internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Mess" + + "ageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [ParamsAllValues] attribute cannot be applied to fields or properties of enum types marked with [Flags]. + /// + internal static string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType_Titl" + + "e", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [ParamsAllValues] attribute can only be applied to a field or property of enum or bool type (or nullable of these types). + /// + internal static string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_MessageFo" + + "rmat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [ParamsAllValues] attribute is only valid on fields or properties of enum or bool type and nullable type for another allowed type. + /// + internal static string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The type of each value provided to the [Params] attribute must match the type of (or be implicitly convertible to) the field or property it is applied to. + /// + internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_Description { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unexpected type for parameter value '{0}'. Expected '{1}' but found '{2}'.. + /// + internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type of all value(s) passed to the [Params] attribute must match the type of (or be implicitly convertible to) the annotated field or property. + /// + internal static string Attributes_ParamsAttribute_MustHaveMatchingValueType_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveMatchingValueType_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [Params] attribute requires at least one value. No values were provided, or an empty array was specified.. + /// + internal static string Attributes_ParamsAttribute_MustHaveValues_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveValues_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [Params] attribute must include at least one value. + /// + internal static string Attributes_ParamsAttribute_MustHaveValues_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_MustHaveValues_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Providing a single value to the [Params] attribute is unnecessary. This attribute is only useful when provided two or more values.. + /// + internal static string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unnecessary single value passed to [Params] attribute. + /// + internal static string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_Title { + get { + return ResourceManager.GetString("Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ParamsSource references a write-only property (a property with only a setter and no getter). Write-only properties cannot be read at runtime and will cause a NullReferenceException. Use a property with a getter or a method instead. + /// + internal static string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Description { + get { + return ResourceManager.GetString("Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ParamsSource cannot reference write-only property '{0}'. Write-only properties cannot be read and will cause a runtime error. + /// + internal static string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ParamsSource cannot reference a write-only property. + /// + internal static string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Title { + get { + return ResourceManager.GetString("Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty_Title", resourceCulture); + } + } + + internal static string Attributes_ParamsSourceAttribute_MustReturnEnumerable_Title { + get { + return ResourceManager.GetString("Attributes_ParamsSourceAttribute_MustReturnEnumerable_Title", resourceCulture); + } + } + + internal static string General_ReservedMemberName_Title { + get { + return ResourceManager.GetString("General_ReservedMemberName_Title", resourceCulture); + } + } + + internal static string General_ReservedMemberName_MessageFormat { + get { + return ResourceManager.GetString("General_ReservedMemberName_MessageFormat", resourceCulture); + } + } + + internal static string General_ReservedMemberName_Description { + get { + return ResourceManager.GetString("General_ReservedMemberName_Description", resourceCulture); + } + } + + internal static string General_BenchmarkClass_RequiredMemberCannotBeSet_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_RequiredMemberCannotBeSet_Title", resourceCulture); + } + } + + internal static string General_BenchmarkClass_RequiredMemberCannotBeSet_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_RequiredMemberCannotBeSet_MessageFormat", resourceCulture); + } + } + + internal static string General_BenchmarkClass_RequiredMemberCannotBeSet_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_RequiredMemberCannotBeSet_Description", resourceCulture); + } + } + + internal static string General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Title", resourceCulture); + } + } + + internal static string General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_MessageFormat", resourceCulture); + } + } + + internal static string General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Description", resourceCulture); + } + } + + internal static string General_Source_AmbiguousEnumerableShape_Title { + get { + return ResourceManager.GetString("General_Source_AmbiguousEnumerableShape_Title", resourceCulture); + } + } + + internal static string General_Source_AmbiguousEnumerableShape_MessageFormat { + get { + return ResourceManager.GetString("General_Source_AmbiguousEnumerableShape_MessageFormat", resourceCulture); + } + } + + internal static string General_Source_AmbiguousEnumerableShape_Description { + get { + return ResourceManager.GetString("General_Source_AmbiguousEnumerableShape_Description", resourceCulture); + } + } + + internal static string General_Source_ElementMayBeByRefLike_Title { + get { + return ResourceManager.GetString("General_Source_ElementMayBeByRefLike_Title", resourceCulture); + } + } + + internal static string General_Source_ElementMayBeByRefLike_MessageFormat { + get { + return ResourceManager.GetString("General_Source_ElementMayBeByRefLike_MessageFormat", resourceCulture); + } + } + + internal static string General_Source_ElementMayBeByRefLike_Description { + get { + return ResourceManager.GetString("General_Source_ElementMayBeByRefLike_Description", resourceCulture); + } + } + + internal static string General_Source_ElementMustNotBeByRefLike_Title { + get { + return ResourceManager.GetString("General_Source_ElementMustNotBeByRefLike_Title", resourceCulture); + } + } + + internal static string General_Source_ElementMustNotBeByRefLike_MessageFormat { + get { + return ResourceManager.GetString("General_Source_ElementMustNotBeByRefLike_MessageFormat", resourceCulture); + } + } + + internal static string General_Source_ElementMustNotBeByRefLike_Description { + get { + return ResourceManager.GetString("General_Source_ElementMustNotBeByRefLike_Description", resourceCulture); + } + } + + internal static string General_Source_MethodMustNotBeGeneric_Title { + get { + return ResourceManager.GetString("General_Source_MethodMustNotBeGeneric_Title", resourceCulture); + } + } + + internal static string General_Source_MethodMustNotBeGeneric_MessageFormat { + get { + return ResourceManager.GetString("General_Source_MethodMustNotBeGeneric_MessageFormat", resourceCulture); + } + } + + internal static string General_Source_MethodMustNotBeGeneric_Description { + get { + return ResourceManager.GetString("General_Source_MethodMustNotBeGeneric_Description", resourceCulture); + } + } + + internal static string General_Source_MethodMustNotHaveRequiredParameters_Title { + get { + return ResourceManager.GetString("General_Source_MethodMustNotHaveRequiredParameters_Title", resourceCulture); + } + } + + internal static string General_Source_MethodMustNotHaveRequiredParameters_MessageFormat { + get { + return ResourceManager.GetString("General_Source_MethodMustNotHaveRequiredParameters_MessageFormat", resourceCulture); + } + } + + internal static string General_Source_MethodMustNotHaveRequiredParameters_Description { + get { + return ResourceManager.GetString("General_Source_MethodMustNotHaveRequiredParameters_Description", resourceCulture); + } + } + + internal static string Attributes_ParamsSourceAttribute_MustReturnEnumerable_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ParamsSourceAttribute_MustReturnEnumerable_MessageFormat", resourceCulture); + } + } + + internal static string Attributes_ParamsSourceAttribute_MustReturnEnumerable_Description { + get { + return ResourceManager.GetString("Attributes_ParamsSourceAttribute_MustReturnEnumerable_Description", resourceCulture); + } + } + + internal static string Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Title { + get { + return ResourceManager.GetString("Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Title", resourceCulture); + } + } + + internal static string Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_MessageFormat { + get { + return ResourceManager.GetString("Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_MessageFormat", resourceCulture); + } + } + + internal static string Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Description { + get { + return ResourceManager.GetString("Attributes_ArgumentsSourceAttribute_MustReturnEnumerable_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A generic benchmark class referenced in the BenchmarkRunner.Run method must be annotated with at least one [GenericTypeArguments] attribute. + /// + internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Description { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + + "ntsAttribute_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Referenced generic benchmark class '{0}' has no [GenericTypeArguments] attribute(s). + /// + internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + + "ntsAttribute_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Generic benchmark classes must be annotated with at least one [GenericTypeArguments] attribute. + /// + internal static string BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgumentsAttribute_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_GenericTypeArgumentClassMustBeAnnotatedWithAGenericTypeArgume" + + "ntsAttribute_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The referenced benchmark class (or any of its inherited classes) must have at least one method annotated with the [Benchmark] attribute. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Description { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Intended benchmark class '{0}' (or any of its ancestors) has no method(s) annotated with the [Benchmark] attribute. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark class (or any of its ancestors) has no annotated method(s). + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMissingBenchmarkMethods_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class referenced in the BenchmarkRunner.Run method must be non-abstract. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Description { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Referenced benchmark class '{0}' cannot be abstract. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes must be non-abstract. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeNonAbstract_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Referenced benchmark class '{0}' must be public. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes must be public. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBePublic_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class referenced in the BenchmarkRunner.Run method must be unsealed. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Description { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Referenced benchmark class '{0}' is sealed. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_MessageFormat { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes must be unsealed. + /// + internal static string BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Title { + get { + return ResourceManager.GetString("BenchmarkRunner_Run_TypeArgumentClassMustBeUnsealed_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class must be an instance class. + /// + internal static string General_BenchmarkClass_ClassMustBeNonStatic_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark class '{0}' cannot be static. + /// + internal static string General_BenchmarkClass_ClassMustBeNonStatic_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes must be non-static. + /// + internal static string General_BenchmarkClass_ClassMustBeNonStatic_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassMustBeNonStatic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A benchmark class annotated with a [GenericTypeArguments] attribute must be generic, having between one to three type parameters. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Descri" + + "ption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attribute [GenericTypeArguments] can only be applied to a generic class. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Messag" + + "eFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark classes annotated with a [GenericTypeArguments] attribute must be generic. + /// + internal static string General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_ClassWithGenericTypeArgumentsAttributeMustBeGeneric_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class. + /// + internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + + "Count_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expected {0} type argument{1} as declared on the benchmark class '{2}', but found {3}. Update the attribute usage or the type parameter list of the class declaration to match.. + /// + internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + + "Count_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class. + /// + internal static string General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameterCount_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_GenericTypeArgumentsAttributeMustHaveMatchingTypeParameter" + + "Count_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A method annotated with the [Benchmark] attribute must be non-generic. + /// + internal static string General_BenchmarkClass_MethodMustBeNonGeneric_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The benchmark method '{0}' must be non-generic. + /// + internal static string General_BenchmarkClass_MethodMustBeNonGeneric_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark methods must be non-generic. + /// + internal static string General_BenchmarkClass_MethodMustBeNonGeneric_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBeNonGeneric_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A method annotated with the [Benchmark] attribute must be public. + /// + internal static string General_BenchmarkClass_MethodMustBePublic_Description { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The benchmark method '{0}' must be public. + /// + internal static string General_BenchmarkClass_MethodMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Benchmark methods must be public. + /// + internal static string General_BenchmarkClass_MethodMustBePublic_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_MethodMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one benchmark method can be marked as baseline per class. + /// + internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaseline_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaseline_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one benchmark method can be baseline per class. + /// + internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaseline_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaseline_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one benchmark method can be marked as baseline per class and category. + /// + internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Only one benchmark method can be baseline per class and category. + /// + internal static string General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passing a single null argument creates a null params array. Use multiple arguments (e.g., null, "SomeCategory", ...) or a non-null value instead.. + /// + internal static string General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_MessageFormat { + get { + return ResourceManager.GetString("General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_M" + + "essageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Single null argument to the [BenchmarkCategory] attribute results in unintended null array. + /// + internal static string General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_Title { + get { + return ResourceManager.GetString("General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed_T" + + "itle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fields or properties annotated with [BenchmarkCancellation] must be of type CancellationToken. + /// + internal static string Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_Title { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field or property '{0}' annotated with [BenchmarkCancellation] is of type '{1}' but must be of type 'System.Threading.CancellationToken'. + /// + internal static string Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_MessageFormat { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_MessageFor" + + "mat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [BenchmarkCancellation] attribute can only be applied to fields or properties of type System.Threading.CancellationToken. + /// + internal static string Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_Description { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType_Description" + + "", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Fields annotated with [BenchmarkCancellation] must be public. + /// + internal static string Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_Title { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field '{0}' annotated with [{1}] must be public. + /// + internal static string Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [BenchmarkCancellation] attribute requires the field to be public so BenchmarkDotNet can inject the cancellation token. + /// + internal static string Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_Description { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_FieldMustBePublic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties annotated with [BenchmarkCancellation] must be public. + /// + internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_Title { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must be public. + /// + internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_MessageFormat { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [BenchmarkCancellation] attribute requires the property to be public so BenchmarkDotNet can inject the cancellation token. + /// + internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_Description { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [BenchmarkCancellation] attribute is not valid on readonly fields. + /// + internal static string Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_Title { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Field '{0}' annotated with [{1}] cannot be readonly. + /// + internal static string Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_MessageFormat { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [BenchmarkCancellation] attribute cannot be applied to readonly fields because BenchmarkDotNet needs to set the field value at runtime. + /// + internal static string Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_Description { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_NotValidOnReadonlyField_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties annotated with [BenchmarkCancellation] must have a public setter. + /// + internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_Title { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' annotated with [{1}] must have a public setter. + /// + internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_MessageFormat { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_MessageFor" + + "mat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The [BenchmarkCancellation] attribute requires a public setter so BenchmarkDotNet can inject the cancellation token. Init-only setters are supported.. + /// + internal static string Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_Description { + get { + return ResourceManager.GetString("Attributes_BenchmarkCancellationAttribute_PropertyMustHavePublicSetter_Descriptio" + + "n", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Async benchmarks should have a [BenchmarkCancellation] property for cancellation support. + /// + internal static string General_AsyncBenchmark_ShouldHaveCancellationToken_Title { + get { + return ResourceManager.GetString("General_AsyncBenchmark_ShouldHaveCancellationToken_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Class '{0}' contains async benchmark methods but no [BenchmarkCancellation] property. Consider adding a public CancellationToken property with [BenchmarkCancellation] attribute to support benchmark cancellation.. + /// + internal static string General_AsyncBenchmark_ShouldHaveCancellationToken_MessageFormat { + get { + return ResourceManager.GetString("General_AsyncBenchmark_ShouldHaveCancellationToken_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Async benchmark methods should check for cancellation to allow graceful termination. Add a public property 'public CancellationToken CancellationToken { get; set; }' annotated with [BenchmarkCancellation] to enable this functionality.. + /// + internal static string General_AsyncBenchmark_ShouldHaveCancellationToken_Description { + get { + return ResourceManager.GetString("General_AsyncBenchmark_ShouldHaveCancellationToken_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Setup and cleanup methods must not return an async enumerable. + /// + internal static string Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_Title { + get { + return ResourceManager.GetString("Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [{0}] method '{1}' returns an async enumerable, which is not supported. + /// + internal static string Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_MessageFormat { + get { + return ResourceManager.GetString("Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to BenchmarkDotNet awaits awaitable return values of [GlobalSetup], [GlobalCleanup], [IterationSetup], and [IterationCleanup] methods, but it does not enumerate async enumerables — the iterator body would silently never run. Change the return type to void, Task, ValueTask, or another awaitable.. + /// + internal static string Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_Description { + get { + return ResourceManager.GetString("Attributes_SetupCleanup_MustNotReturnAsyncEnumerable_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Return type is both awaitable and an async enumerable. + /// + internal static string General_AwaitableAsyncEnumerable_AmbiguousReturnType_Title { + get { + return ResourceManager.GetString("General_AwaitableAsyncEnumerable_AmbiguousReturnType_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [{0}] method '{1}' returns '{2}', which is both awaitable and an async enumerable; BenchmarkDotNet awaits the value and never enumerates it. + /// + internal static string General_AwaitableAsyncEnumerable_AmbiguousReturnType_MessageFormat { + get { + return ResourceManager.GetString("General_AwaitableAsyncEnumerable_AmbiguousReturnType_MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to When a benchmark or setup/cleanup return type satisfies both the awaitable pattern (a public GetAwaiter) and the async-enumerable pattern (a public GetAsyncEnumerator), BenchmarkDotNet treats it as awaitable — the iterator's body is never executed. Pick one shape so the intent is unambiguous: drop GetAwaiter if you want it consumed as an async enumerable, or drop GetAsyncEnumerator if you want it awaited.. + /// + internal static string General_AwaitableAsyncEnumerable_AmbiguousReturnType_Description { + get { + return ResourceManager.GetString("General_AwaitableAsyncEnumerable_AmbiguousReturnType_Description", resourceCulture); + } + } + } +} diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx index 7194f15210..ce8dbc079c 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkDotNetAnalyzerResources.resx @@ -1,454 +1,535 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The referenced benchmark class (or any of its inherited classes) must have at least one method annotated with the [Benchmark] attribute - - - Intended benchmark class '{0}' (or any of its ancestors) has no method(s) annotated with the [Benchmark] attribute - - - Referenced benchmark class '{0}' cannot be abstract - - - Benchmark class (or any of its ancestors) has no annotated method(s) - - - Benchmark classes must be non-abstract - - - A benchmark class must be an instance class - - - Benchmark class '{0}' cannot be static - - - Referenced generic benchmark class '{0}' has no [GenericTypeArguments] attribute(s) - - - A generic benchmark class referenced in the BenchmarkRunner.Run method must be annotated with at least one [GenericTypeArguments] attribute - - - Benchmark classes must be non-static - - - Generic benchmark classes must be annotated with at least one [GenericTypeArguments] attribute - - - Benchmark classes must be public - - - A benchmark class referenced in the BenchmarkRunner.Run method must be unsealed - - - Referenced benchmark class '{0}' is sealed - - - Benchmark classes must be unsealed - - - A method annotated with the [Benchmark] attribute must be public - - - A method annotated with the [Benchmark] attribute must be non-generic - - - The number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class - - - A benchmark class annotated with a [GenericTypeArguments] attribute must be generic, having between one to three type parameters - - - The benchmark method '{0}' must be public - - - The benchmark method '{0}' must be non-generic - - - Expected {0} type argument{1} as declared on the benchmark class '{2}', but found {3}. Update the attribute usage or the type parameter list of the class declaration to match. - - - Attribute [GenericTypeArguments] can only be applied to a generic class - - - Only one benchmark method can be marked as baseline per class - - - Only one benchmark method can be marked as baseline per class and category - - - Passing a single null argument creates a null params array. Use multiple arguments (e.g., null, "SomeCategory", ...) or a non-null value instead. - - - Benchmark methods must be public - - - Benchmark methods must be non-generic - - - Number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class - - - Benchmark classes annotated with a [GenericTypeArguments] attribute must be generic - - - Only one benchmark method can be baseline per class - - - Only one benchmark method can be baseline per class and category - - - Single null argument to the [BenchmarkCategory] attribute results in unintended null array - - - Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a field at any one time - - - A field annotated with a parameter attribute must be public - - - A property annotated with a parameter attribute must be public - - - A property annotated with a parameter attribute must have a public setter; make sure that the access modifier of the setter is empty and that the property is not an auto-property or an expression-bodied property. - - - The type of each value provided to the [Params] attribute must match the type of (or be implicitly convertible to) the field or property it is applied to - - - The [ParamsAllValues] attribute cannot be applied to a field or property of an enum type marked with the [Flags] attribute. Use this attribute only with non-flags enum types, as [Flags] enums support bitwise combinations that cannot be exhaustively enumerated. - - - A property annotated with a parameter attribute must have a public, assignable setter i.e. { set; } - - - Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a property at any one time - - - Duplicate parameter attribute on field '{0}' - - - Field '{0}' annotated with [{1}] must be public - - - Expected {0} value{1} as declared by the benchmark method '{2}', but found {3}. Update the attribute usage or method to match. - - - Benchmark method '{0}' without an [ArgumentsSource] or [Arguments] attribute(s) cannot declare parameters - - - Unexpected type for argument value '{0}'. Expected '{1}' but found '{2}'. - - - Property '{0}' annotated with [{1}] must be public - - - Property '{0}' annotated with [{1}] must have a public setter - - - The [Params] attribute requires at least one value. No values were provided, or an empty array was specified. - - - Providing a single value to the [Params] attribute is unnecessary. This attribute is only useful when provided two or more values. - - - Unexpected type for parameter value '{0}'. Expected '{1}' but found '{2}'. - - - Field or property enum type '{0}' is marked with [Flags] and cannot be used with this attribute - - - The [ParamsAllValues] attribute can only be applied to a field or property of enum or bool type (or nullable of these types) - - - ParamsSource cannot reference a write-only property - - - ParamsSource cannot reference write-only property '{0}'. Write-only properties cannot be read and will cause a runtime error. - - - ParamsSource references a write-only property (a property with only a setter and no getter). Write-only properties cannot be read at runtime and will cause a NullReferenceException. Use a property with a getter or a method instead. - - - Property '{0}' annotated with [{1}] cannot be init-only - - - Duplicate parameter attribute on property '{0}' - - - Only one parameter attribute can be applied to a field - - - Fields annotated with a parameter attribute must be public - - - Number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method - - - Values passed to an [Arguments] attribute must match exactly the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order - - - Properties annotated with a parameter attribute must be public - - - Properties annotated with a parameter attribute must have a public setter - - - The [Params] attribute must include at least one value - - - Unnecessary single value passed to [Params] attribute - - - Type of all value(s) passed to the [Params] attribute must match the type of (or be implicitly convertible to) the annotated field or property - - - The [ParamsAllValues] attribute cannot be applied to fields or properties of enum types marked with [Flags] - - - The [ParamsAllValues] attribute is only valid on fields or properties of enum or bool type and nullable type for another allowed type - - - Properties annotated with a parameter attribute cannot have an init-only setter - - - Only one parameter attribute can be applied to a property - - - Parameter attributes are not valid on fields with a readonly modifier - - - Fields annotated with a parameter attribute cannot be read-only - - - Parameter attributes are not valid on constant field declarations - - - Modifier 'readonly' is not valid on field '{0}' annotated with parameter attribute [{1}] - - - Parameter attribute [{0}] is not valid on constants. It is only valid on non-constant field declarations. - - - The number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method - - - This method declares one or more parameters but is not annotated with 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. - - - 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(Source)] 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 - - - Referenced benchmark class '{0}' must be public - - - 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 - - - The values passed to an [Arguments] must have parameter(s) - - - Fields or properties annotated with [BenchmarkCancellation] must be of type CancellationToken - - - Field or property '{0}' annotated with [BenchmarkCancellation] is of type '{1}' but must be of type 'System.Threading.CancellationToken' - - - The [BenchmarkCancellation] attribute can only be applied to fields or properties of type System.Threading.CancellationToken - - - Fields annotated with [BenchmarkCancellation] must be public - - - Field '{0}' annotated with [{1}] must be public - - - The [BenchmarkCancellation] attribute requires the field to be public so BenchmarkDotNet can inject the cancellation token - - - Properties annotated with [BenchmarkCancellation] must be public - - - Property '{0}' annotated with [{1}] must be public - - - The [BenchmarkCancellation] attribute requires the property to be public so BenchmarkDotNet can inject the cancellation token - - - [BenchmarkCancellation] attribute is not valid on readonly fields - - - Field '{0}' annotated with [{1}] cannot be readonly - - - The [BenchmarkCancellation] attribute cannot be applied to readonly fields because BenchmarkDotNet needs to set the field value at runtime - - - Properties annotated with [BenchmarkCancellation] must have a public setter - - - Property '{0}' annotated with [{1}] must have a public setter - - - The [BenchmarkCancellation] attribute requires a public setter so BenchmarkDotNet can inject the cancellation token. Init-only setters are supported. - - - Async benchmarks should have a [BenchmarkCancellation] property for cancellation support - - - Class '{0}' contains async benchmark methods but no [BenchmarkCancellation] property. Consider adding a public CancellationToken property with [BenchmarkCancellation] attribute to support benchmark cancellation. - - - Async benchmark methods should check for cancellation to allow graceful termination. Add a public property 'public CancellationToken CancellationToken { get; set; }' annotated with [BenchmarkCancellation] to enable this functionality. - - - Setup and cleanup methods must not return an async enumerable - - - [{0}] method '{1}' returns an async enumerable, which is not supported - - - BenchmarkDotNet awaits awaitable return values of [GlobalSetup], [GlobalCleanup], [IterationSetup], and [IterationCleanup] methods, but it does not enumerate async enumerables — the iterator body would silently never run. Change the return type to void, Task, ValueTask, or another awaitable. - - - Return type is both awaitable and an async enumerable - - - [{0}] method '{1}' returns '{2}', which is both awaitable and an async enumerable; BenchmarkDotNet awaits the value and never enumerates it - - - When a benchmark or setup/cleanup return type satisfies both the awaitable pattern (a public GetAwaiter) and the async-enumerable pattern (a public GetAsyncEnumerator), BenchmarkDotNet treats it as awaitable — the iterator's body is never executed. Pick one shape so the intent is unambiguous: drop GetAwaiter if you want it consumed as an async enumerable, or drop GetAsyncEnumerator if you want it awaited. - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The referenced benchmark class (or any of its inherited classes) must have at least one method annotated with the [Benchmark] attribute + + + Intended benchmark class '{0}' (or any of its ancestors) has no method(s) annotated with the [Benchmark] attribute + + + Referenced benchmark class '{0}' cannot be abstract + + + Benchmark class (or any of its ancestors) has no annotated method(s) + + + Benchmark classes must be non-abstract + + + A benchmark class must be an instance class + + + Benchmark class '{0}' cannot be static + + + Referenced generic benchmark class '{0}' has no [GenericTypeArguments] attribute(s) + + + A generic benchmark class referenced in the BenchmarkRunner.Run method must be annotated with at least one [GenericTypeArguments] attribute + + + Benchmark classes must be non-static + + + Generic benchmark classes must be annotated with at least one [GenericTypeArguments] attribute + + + Benchmark classes must be public + + + A benchmark class referenced in the BenchmarkRunner.Run method must be unsealed + + + Referenced benchmark class '{0}' is sealed + + + Benchmark classes must be unsealed + + + A method annotated with the [Benchmark] attribute must be public + + + A method annotated with the [Benchmark] attribute must be non-generic + + + The number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class + + + A benchmark class annotated with a [GenericTypeArguments] attribute must be generic, having between one to three type parameters + + + The benchmark method '{0}' must be public + + + The benchmark method '{0}' must be non-generic + + + Expected {0} type argument{1} as declared on the benchmark class '{2}', but found {3}. Update the attribute usage or the type parameter list of the class declaration to match. + + + Attribute [GenericTypeArguments] can only be applied to a generic class + + + Only one benchmark method can be marked as baseline per class + + + Only one benchmark method can be marked as baseline per class and category + + + Passing a single null argument creates a null params array. Use multiple arguments (e.g., null, "SomeCategory", ...) or a non-null value instead. + + + Benchmark methods must be public + + + Benchmark methods must be non-generic + + + Number of type arguments passed to a [GenericTypeArguments] attribute must match the number of type parameters on the targeted benchmark class + + + Benchmark classes annotated with a [GenericTypeArguments] attribute must be generic + + + Only one benchmark method can be baseline per class + + + Only one benchmark method can be baseline per class and category + + + Single null argument to the [BenchmarkCategory] attribute results in unintended null array + + + Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a field at any one time + + + A field annotated with a parameter attribute must be public + + + A property annotated with a parameter attribute must be public + + + A property annotated with a parameter attribute must have a public setter; make sure that the access modifier of the setter is empty and that the property is not an auto-property or an expression-bodied property. + + + The type of each value provided to the [Params] attribute must match the type of (or be implicitly convertible to) the field or property it is applied to + + + The [ParamsAllValues] attribute cannot be applied to a field or property of an enum type marked with the [Flags] attribute. Use this attribute only with non-flags enum types, as [Flags] enums support bitwise combinations that cannot be exhaustively enumerated. + + + Parameter attributes are mutually exclusive; only one of the attributes [Params], [ParamsSource] or [ParamsAllValues] can be applied to a property at any one time + + + Duplicate parameter attribute on field '{0}' + + + Field '{0}' annotated with [{1}] must be public + + + Expected {0} value{1} as declared by the benchmark method '{2}', but found {3}. Update the attribute usage or method to match. + + + Benchmark method '{0}' without an [ArgumentsSource] or [Arguments] attribute(s) cannot declare parameters + + + Unexpected type for argument value '{0}'. Expected '{1}' but found '{2}'. + + + Property '{0}' annotated with [{1}] must be public + + + Property '{0}' annotated with [{1}] must have a public setter + + + The [Params] attribute requires at least one value. No values were provided, or an empty array was specified. + + + Providing a single value to the [Params] attribute is unnecessary. This attribute is only useful when provided two or more values. + + + Unexpected type for parameter value '{0}'. Expected '{1}' but found '{2}'. + + + Field or property enum type '{0}' is marked with [Flags] and cannot be used with this attribute + + + The [ParamsAllValues] attribute can only be applied to a field or property of enum or bool type (or nullable of these types) + + + ParamsSource cannot reference a write-only property + + + ParamsSource cannot reference write-only property '{0}'. Write-only properties cannot be read and will cause a runtime error. + + + ParamsSource references a write-only property (a property with only a setter and no getter). Write-only properties cannot be read at runtime and will cause a NullReferenceException. Use a property with a getter or a method instead. + + + ParamsSource must return a generic enumerable or async enumerable + + + ParamsSource '{0}' must return IEnumerable<T> or IAsyncEnumerable<T>, but returns '{1}'. + + + The member referenced by ParamsSource must return IEnumerable<T> or IAsyncEnumerable<T>. The non-generic IEnumerable is not enough on its own: BenchmarkDotNet's generated code infers the element type from the source, so the return type has to name it. The await-foreach pattern without the IAsyncEnumerable<T> interface, and other return types such as Task<IEnumerable<T>>, are not supported either. + + + Parameter member name is reserved by code generation + + + '{0}' is a reserved name used by BenchmarkDotNet's code generation and cannot be used for a member with {1}. Please, rename the member. + + + BenchmarkDotNet generates a runnable type that derives from the benchmark type and assigns each [Params]/[ParamsSource]/[ParamsAllValues] member through an object initializer. A parameter member whose name matches one of the generated members (e.g. __GlobalSetup, __WorkloadActionUnroll) binds to the generated member instead and makes the generated program fail to compile. Rename the member. + + + Required member cannot be set by BenchmarkDotNet + + + '{0}' is a required member that BenchmarkDotNet cannot set. Only [Params]/[ParamsSource]/[ParamsAllValues] and [BenchmarkCancellation] members are set when the benchmark is constructed. Please, remove the 'required' modifier or apply one of those attributes. + + + BenchmarkDotNet constructs the benchmark type and can only set required members at construction time: [Params]/[ParamsSource]/[ParamsAllValues] members and an instance [BenchmarkCancellation] member. A required member set later (e.g. in [GlobalSetup]) or not set at all makes the generated program fail to compile. Remove the 'required' modifier or apply one of the supported attributes. + + + Benchmark constructor must not be annotated with [SetsRequiredMembers] + + + The constructor of '{0}' is annotated with [SetsRequiredMembers], which BenchmarkDotNet's generated constructor cannot honor. Please, remove the attribute and set each required member with [Params]/[ParamsSource]/[ParamsAllValues] or [BenchmarkCancellation]. + + + BenchmarkDotNet generates a runnable type deriving from the benchmark type, whose constructor chains to the benchmark type's constructor. C# requires that constructor to repeat [SetsRequiredMembers] (CS9039), which would suppress all required-member checking and silently hide required members BenchmarkDotNet cannot set. Remove the attribute and set each required member with [Params]/[ParamsSource]/[ParamsAllValues] or [BenchmarkCancellation]. + + + Source must not have more than one enumerable shape + + + The source '{0}' returns '{1}', which has more than one enumerable shape, so BenchmarkDotNet cannot tell which one to read the values from. Please, return a single IEnumerable<T> or IAsyncEnumerable<T>. + + + A [ParamsSource]/[ArgumentsSource] member whose return type offers several candidate shapes is ambiguous - both IEnumerable<T> and IAsyncEnumerable<T>, or several instantiations of either one, such as IEnumerable<int> together with IEnumerable<string>. BenchmarkDotNet's generated code infers the element type from the source, and type inference requires a unique candidate, so the generated code fails to compile. This holds even when one element type converts to the other. Return a single enumerable or async enumerable. + + + A source method must not be generic + + + Source '{0}' resolves only to a generic method, which BenchmarkDotNet cannot invoke. Please, make the method non-generic, or add a non-generic overload taking no required parameters. + + + BenchmarkDotNet invokes a source method directly, and nothing supplies a generic method's type arguments - there is no call site to infer them from. Invoking one fails inside reflection with a message that says nothing about the benchmark, so the declaration is reported instead. A non-generic overload whose parameters are all optional, or a property with a public getter, serves as the source where one exists. + + + A source must not yield a ref struct + + + Source '{0}' is declared to yield '{1}', which {2}. BenchmarkDotNet reads a source's values through reflection, which cannot carry one. Please, yield a type the values can be built from - an array for a Span parameter - and let the benchmark take the ref struct. + + + BenchmarkDotNet reads a source's values into an object[] during discovery, so every value has to survive being boxed, and a ref struct cannot be. Since .NET 10 the framework's IEnumerable<T> allows a ref struct type argument, so such a source compiles and then fails inside reflection with a message that says nothing about the benchmark. A source yielding a type parameter constrained with 'allows ref struct' is reported the same way: an open declaration is judged on what every type argument guarantees, and that constraint guarantees nothing about boxing. A ref struct parameter is supported and remains so: declare the source to yield what the value is built from, such as IEnumerable<byte[]> for a ReadOnlySpan<byte> parameter, which the generated code converts. + + + A source may yield a ref struct + + + Source '{0}' is declared to yield '{1}', which {2}. A type argument that is not by-ref-like reads normally; one that is fails while BenchmarkDotNet reads the values. Please, yield a type the values can be built from - an array for a Span parameter - and let the benchmark take the ref struct. + + + BenchmarkDotNet reads a source's values into an object[] during discovery, so every value has to survive being boxed, and a ref struct cannot be. A source yielding a type parameter constrained with 'allows ref struct' says nothing about whether any particular type argument is one: closed to int it reads like any other value type, and closed to a ref struct it fails while the values are read, which BenchmarkDotNet reports naming the element. The compiler cannot tell which type arguments a benchmark will be given, so this is a warning rather than an error - unlike BDN1311, which reports a ref struct it can see. A ref struct parameter is supported and remains so: declare the source to yield what the value is built from, such as IEnumerable<byte[]> for a ReadOnlySpan<byte> parameter, which the generated code converts. + + + Source method must not have required parameters + + + The source '{0}' has required parameters. A [ParamsSource]/[ArgumentsSource] source method must be parameterless or have only optional parameters. Please, make its parameters optional. + + + A method referenced by [ParamsSource] or [ArgumentsSource] is invoked by BenchmarkDotNet with no arguments (optional parameters, such as an [EnumeratorCancellation] token, receive their defaults). A source method with a required parameter cannot be invoked and is not recognized as a source. Make its parameters optional or remove them. + + + ArgumentsSource must return a generic enumerable or async enumerable + + + ArgumentsSource '{0}' must return IEnumerable<T> or IAsyncEnumerable<T>, but returns '{1}'. + + + The member referenced by ArgumentsSource must return IEnumerable<T> or IAsyncEnumerable<T>. The non-generic IEnumerable is not enough on its own: BenchmarkDotNet's generated code infers the element type from the source, so the return type has to name it. The await-foreach pattern without the IAsyncEnumerable<T> interface, and other return types such as Task<IEnumerable<T>>, are not supported either. + + + Duplicate parameter attribute on property '{0}' + + + Only one parameter attribute can be applied to a field + + + Fields annotated with a parameter attribute must be public + + + Number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method + + + Values passed to an [Arguments] attribute must match exactly the parameters declared in the targeted benchmark method in both type (or be implicitly convertible to) and order + + + Properties annotated with a parameter attribute must be public + + + Properties annotated with a parameter attribute must have a public setter + + + The [Params] attribute must include at least one value + + + Unnecessary single value passed to [Params] attribute + + + Type of all value(s) passed to the [Params] attribute must match the type of (or be implicitly convertible to) the annotated field or property + + + The [ParamsAllValues] attribute cannot be applied to fields or properties of enum types marked with [Flags] + + + The [ParamsAllValues] attribute is only valid on fields or properties of enum or bool type and nullable type for another allowed type + + + Only one parameter attribute can be applied to a property + + + Parameter attributes are not valid on fields with a readonly modifier + + + Fields annotated with a parameter attribute cannot be read-only + + + Parameter attributes are not valid on constant field declarations + + + Modifier 'readonly' is not valid on field '{0}' annotated with parameter attribute [{1}] + + + Parameter attribute [{0}] is not valid on constants. It is only valid on non-constant field declarations. + + + The number of values passed to an [Arguments] attribute must match the number of parameters declared in the targeted benchmark method + + + This method declares one or more parameters but is not annotated with 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. + + + 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(Source)] 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 + + + Referenced benchmark class '{0}' must be public + + + 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 + + + The values passed to an [Arguments] must have parameter(s) + + + Fields or properties annotated with [BenchmarkCancellation] must be of type CancellationToken + + + Field or property '{0}' annotated with [BenchmarkCancellation] is of type '{1}' but must be of type 'System.Threading.CancellationToken' + + + The [BenchmarkCancellation] attribute can only be applied to fields or properties of type System.Threading.CancellationToken + + + Fields annotated with [BenchmarkCancellation] must be public + + + Field '{0}' annotated with [{1}] must be public + + + The [BenchmarkCancellation] attribute requires the field to be public so BenchmarkDotNet can inject the cancellation token + + + Properties annotated with [BenchmarkCancellation] must be public + + + Property '{0}' annotated with [{1}] must be public + + + The [BenchmarkCancellation] attribute requires the property to be public so BenchmarkDotNet can inject the cancellation token + + + [BenchmarkCancellation] attribute is not valid on readonly fields + + + Field '{0}' annotated with [{1}] cannot be readonly + + + The [BenchmarkCancellation] attribute cannot be applied to readonly fields because BenchmarkDotNet needs to set the field value at runtime + + + Properties annotated with [BenchmarkCancellation] must have a public setter + + + Property '{0}' annotated with [{1}] must have a public setter + + + The [BenchmarkCancellation] attribute requires a public setter so BenchmarkDotNet can inject the cancellation token. Init-only setters are supported. + + + Async benchmarks should have a [BenchmarkCancellation] property for cancellation support + + + Class '{0}' contains async benchmark methods but no [BenchmarkCancellation] property. Consider adding a public CancellationToken property with [BenchmarkCancellation] attribute to support benchmark cancellation. + + + Async benchmark methods should check for cancellation to allow graceful termination. Add a public property 'public CancellationToken CancellationToken { get; set; }' annotated with [BenchmarkCancellation] to enable this functionality. + + + Setup and cleanup methods must not return an async enumerable + + + [{0}] method '{1}' returns an async enumerable, which is not supported + + + BenchmarkDotNet awaits awaitable return values of [GlobalSetup], [GlobalCleanup], [IterationSetup], and [IterationCleanup] methods, but it does not enumerate async enumerables — the iterator body would silently never run. Change the return type to void, Task, ValueTask, or another awaitable. + + + Return type is both awaitable and an async enumerable + + + [{0}] method '{1}' returns '{2}', which is both awaitable and an async enumerable; BenchmarkDotNet awaits the value and never enumerates it + + + When a benchmark or setup/cleanup return type satisfies both the awaitable pattern (a public GetAwaiter) and the async-enumerable pattern (a public GetAsyncEnumerator), BenchmarkDotNet treats it as awaitable — the iterator's body is never executed. Pick one shape so the intent is unambiguous: drop GetAwaiter if you want it consumed as an async enumerable, or drop GetAsyncEnumerator if you want it awaited. + \ No newline at end of file diff --git a/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs index 6dd467fc56..63b188769b 100644 --- a/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/BenchmarkRunner/RunAnalyzer.cs @@ -202,7 +202,7 @@ bool HasBenchmarkAttribute() { if (attributeData.AttributeClass != null) { - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, benchmarkAttributeTypeSymbol)) + if (AnalyzerHelper.IsOrDerivesFrom(attributeData.AttributeClass, benchmarkAttributeTypeSymbol)) { return true; } diff --git a/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs b/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs index 56eca0fb3b..ed876a5b98 100644 --- a/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs +++ b/src/BenchmarkDotNet.Analyzers/DiagnosticIds.cs @@ -15,25 +15,34 @@ public static class DiagnosticIds public const string General_BenchmarkClass_SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed = "BDN1106"; public const string General_BenchmarkClass_OnlyOneMethodCanBeBaseline = "BDN1107"; public const string General_BenchmarkClass_OnlyOneMethodCanBeBaselinePerCategory = "BDN1108"; + public const string General_BenchmarkClass_RequiredMemberCannotBeSet = "BDN1109"; + public const string General_BenchmarkClass_ConstructorMustNotSetRequiredMembers = "BDN1110"; public const string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnField = "BDN1200"; public const string Attributes_GeneralParameterAttributes_MutuallyExclusiveOnProperty = "BDN1201"; public const string Attributes_GeneralParameterAttributes_FieldMustBePublic = "BDN1202"; public const string Attributes_GeneralParameterAttributes_PropertyMustBePublic = "BDN1203"; public const string Attributes_GeneralParameterAttributes_NotValidOnReadonlyField = "BDN1204"; public const string Attributes_GeneralParameterAttributes_NotValidOnConstantField = "BDN1205"; - public const string Attributes_GeneralParameterAttributes_PropertyCannotBeInitOnly = "BDN1206"; public const string Attributes_GeneralParameterAttributes_PropertyMustHavePublicSetter = "BDN1207"; + public const string General_ReservedMemberName = "BDN1208"; public const string Attributes_ParamsAttribute_MustHaveValues = "BDN1300"; public const string Attributes_ParamsAttribute_MustHaveMatchingValueType = "BDN1301"; public const string Attributes_ParamsAttribute_UnnecessarySingleValuePassedToAttribute = "BDN1302"; public const string Attributes_ParamsAllValuesAttribute_NotAllowedOnFlagsEnumPropertyOrFieldType = "BDN1303"; public const string Attributes_ParamsAllValuesAttribute_PropertyOrFieldTypeMustBeEnumOrBool = "BDN1304"; public const string Attributes_ParamsSourceAttribute_CannotUseWriteOnlyProperty = "BDN1305"; + public const string Attributes_ParamsSourceAttribute_MustReturnEnumerable = "BDN1306"; + public const string General_Source_MethodMustNotHaveRequiredParameters = "BDN1307"; + public const string General_Source_AmbiguousEnumerableShape = "BDN1308"; + public const string General_Source_MethodMustNotBeGeneric = "BDN1310"; + public const string General_Source_ElementMustNotBeByRefLike = "BDN1311"; + public const string General_Source_ElementMayBeByRefLike = "BDN1312"; public const string Attributes_GeneralArgumentAttributes_MethodWithoutAttributeMustHaveNoParameters = "BDN1400"; 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"; + public const string Attributes_ArgumentsSourceAttribute_MustReturnEnumerable = "BDN1504"; public const string Attributes_BenchmarkCancellationAttribute_MustBeCancellationTokenType = "BDN1600"; public const string Attributes_BenchmarkCancellationAttribute_FieldMustBePublic = "BDN1601"; public const string Attributes_BenchmarkCancellationAttribute_PropertyMustBePublic = "BDN1602"; diff --git a/src/BenchmarkDotNet.Analyzers/General/AsyncBenchmarkAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/General/AsyncBenchmarkAnalyzer.cs index ad4af424f8..ba825dfb67 100644 --- a/src/BenchmarkDotNet.Analyzers/General/AsyncBenchmarkAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/General/AsyncBenchmarkAnalyzer.cs @@ -75,7 +75,7 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { var attributeSyntaxTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; if (attributeSyntaxTypeSymbol != null && - SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkAttributeTypeSymbol)) + AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, benchmarkAttributeTypeSymbol)) { hasBenchmarkAttribute = true; break; @@ -112,7 +112,7 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { foreach (var attribute in member.GetAttributes()) { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, benchmarkCancellationAttributeTypeSymbol)) + if (AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, benchmarkCancellationAttributeTypeSymbol)) { hasCancellationTokenMember = true; break; diff --git a/src/BenchmarkDotNet.Analyzers/General/AwaitableAsyncEnumerableAmbiguityAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/General/AwaitableAsyncEnumerableAmbiguityAnalyzer.cs index 8686d6615b..83f711fd36 100644 --- a/src/BenchmarkDotNet.Analyzers/General/AwaitableAsyncEnumerableAmbiguityAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/General/AwaitableAsyncEnumerableAmbiguityAnalyzer.cs @@ -92,7 +92,7 @@ private static void AnalyzeMethod( { foreach (var candidate in captured.AttributeSymbols) { - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, candidate)) + if (AnalyzerHelper.IsOrDerivesFrom(attributeData.AttributeClass, candidate)) { matchedAttribute = candidate; break; diff --git a/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs index 84f5be7e4b..20c1c6206b 100644 --- a/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs +++ b/src/BenchmarkDotNet.Analyzers/General/BenchmarkClassAnalyzer.cs @@ -131,7 +131,8 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) } } - var genericTypeArgumentsAttributes = AnalyzerHelper.GetAttributes("BenchmarkDotNet.Attributes.GenericTypeArgumentsAttribute", context.Compilation, classDeclarationSyntax.AttributeLists, context.SemanticModel); + var genericTypeArgumentsAttributeTypeSymbol = context.Compilation.GetTypeByMetadataName("BenchmarkDotNet.Attributes.GenericTypeArgumentsAttribute"); + var genericTypeArgumentsAttributes = AnalyzerHelper.GetAttributes(genericTypeArgumentsAttributeTypeSymbol, classDeclarationSyntax.AttributeLists, context.SemanticModel); if (genericTypeArgumentsAttributes.Length > 0) { foreach (var genericTypeArgumentsAttribute in genericTypeArgumentsAttributes) @@ -140,7 +141,11 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create(ClassWithGenericTypeArgumentsAttributeMustBeGenericRule, genericTypeArgumentsAttribute.GetLocation())); } - else if (genericTypeArgumentsAttribute.ArgumentList is { Arguments.Count: > 0 }) + // Only [GenericTypeArguments] itself is guaranteed to carry the type arguments in its own constructor + // arguments. A derived attribute declares whatever constructor it likes and may hand them to base(...), + // where they are invisible here, so its arguments are not the ones to count. + else if (genericTypeArgumentsAttribute.ArgumentList is { Arguments.Count: > 0 } + && SymbolEqualityComparer.Default.Equals(context.SemanticModel.GetTypeInfo(genericTypeArgumentsAttribute).Type, genericTypeArgumentsAttributeTypeSymbol)) { if (genericTypeArgumentsAttribute.ArgumentList.Arguments.Count != classDeclarationSyntax.TypeParameterList.Parameters.Count) { @@ -190,12 +195,22 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) continue; } - if (SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkAttributeTypeSymbol)) + if (AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, benchmarkAttributeTypeSymbol)) { benchmarkAttributeUsages.Add(attributeSyntax); } - else if (SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkCategoryAttributeTypeSymbol)) + else if (AnalyzerHelper.IsOrDerivesFrom(attributeSyntaxTypeSymbol, benchmarkCategoryAttributeTypeSymbol)) { + // Only [BenchmarkCategory] itself carries the categories in its own arguments; a derived + // attribute may hand them to base(...), where they are invisible. Harvesting its own + // arguments would report the wrong category set rather than an unknown one. + if (!SymbolEqualityComparer.Default.Equals(attributeSyntaxTypeSymbol, benchmarkCategoryAttributeTypeSymbol)) + { + hasBenchmarkCategoryCompilerDiagnostics = true; + + continue; + } + if (attributeSyntax.ArgumentList is { Arguments.Count: 1 }) { // Check if this is an explicit params array creation @@ -435,12 +450,23 @@ private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) foreach (var attribute in methodAttributes) { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, benchmarkAttributeTypeSymbol)) + if (AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, benchmarkAttributeTypeSymbol)) { benchmarkAttributeUsages.Add(attribute); } - else if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, benchmarkCategoryAttributeTypeSymbol)) + else if (AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, benchmarkCategoryAttributeTypeSymbol)) { + // Only [BenchmarkCategory] itself is guaranteed to carry the categories in its own + // constructor arguments; a derived attribute may hand them to base(...), where they + // are invisible here. The categories are then unknown rather than absent, so treat + // the method as unanalyzable instead of as uncategorized. + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, benchmarkCategoryAttributeTypeSymbol)) + { + hasBenchmarkCategoryCompilerDiagnostics = true; + + break; + } + foreach (var benchmarkCategoriesArray in attribute.ConstructorArguments) { if (!benchmarkCategoriesArray.IsNull) @@ -532,6 +558,8 @@ private static void AnalyzeAttributeSyntax(SyntaxNodeAnalysisContext context) var benchmarkCategoryAttributeTypeSymbol = GetBenchmarkCategoryAttributeTypeSymbol(context.Compilation); var attributeTypeSymbol = context.SemanticModel.GetTypeInfo(attributeSyntax).Type; + // Only [BenchmarkCategory] itself: this rule is about a null *category*, and a derived attribute's single + // argument is whatever its own constructor takes - reporting on it flags an argument that is not a category. if (SymbolEqualityComparer.Default.Equals(attributeTypeSymbol, benchmarkCategoryAttributeTypeSymbol)) { if (attributeSyntax.ArgumentList is { Arguments.Count: 1 }) diff --git a/src/BenchmarkDotNet.Analyzers/Polyfills/RefLikeTypePolyfill.cs b/src/BenchmarkDotNet.Analyzers/Polyfills/RefLikeTypePolyfill.cs new file mode 100644 index 0000000000..21f1911e42 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/Polyfills/RefLikeTypePolyfill.cs @@ -0,0 +1,41 @@ +#if !CODE_ANALYSIS_3_0 +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp; + +namespace Microsoft.CodeAnalysis; + +// ITypeSymbol.IsRefLikeType only appears in Roslyn 3.0 (dotnet/roslyn#30426), though the compiler has tracked +// ref-likeness since C# 7.2 - as the internal TypeSymbol.IsByRefLikeType this reads. Reflection rather than the +// metadata attribute because that route is deliberately closed: PENamedTypeSymbol.GetAttributes filters +// IsByRefLikeAttribute out exactly when the type is ref-like, so a referenced Span reports no such attribute. +// The property is declared on the base and overridden per symbol kind, so one lookup serves every kind. +// +// Both guards below answer false rather than throwing, because an exception escaping an analyzer is reported as +// AD0001 and disables it. The lookup is guarded because a failure in a static initializer poisons the type for +// every later call; the instance check is guarded because GetValue is the one call here that can throw, and does +// so only for an ITypeSymbol that is not a Roslyn C# symbol - which the language filter on every analyzer here +// makes unreachable, RS1009 forbids implementing, and a runtime mock could still produce. +internal static class RefLikeTypePolyfill +{ + private static readonly PropertyInfo? IsByRefLikeType = ResolveIsByRefLikeType(); + + internal static bool IsRefLikeType(ITypeSymbol type) + => IsByRefLikeType is { } property + && property.DeclaringType!.IsInstanceOfType(type) + && property.GetValue(type) is true; + + private static PropertyInfo? ResolveIsByRefLikeType() + { + try + { + return typeof(CSharpCompilation).Assembly + .GetType("Microsoft.CodeAnalysis.CSharp.Symbols.TypeSymbol") + ?.GetProperty("IsByRefLikeType", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + } + catch + { + return null; + } + } +} +#endif diff --git a/src/BenchmarkDotNet.Analyzers/Polyfills/SymbolEqualityComparer.cs b/src/BenchmarkDotNet.Analyzers/Polyfills/SymbolEqualityComparer.cs new file mode 100644 index 0000000000..ac350dc5fb --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/Polyfills/SymbolEqualityComparer.cs @@ -0,0 +1,19 @@ +#if !CODE_ANALYSIS_3_8 +using System.Collections.Generic; + +namespace Microsoft.CodeAnalysis; + +// SymbolEqualityComparer appears in Roslyn 3.3, so the 2.8 and 3.0 bands compare symbols through ISymbol.Equals, +// as callers did before it existed. Declared in Roslyn's own namespace, so the call sites read the same on +// every band. +internal sealed class SymbolEqualityComparer : IEqualityComparer +{ + internal static readonly SymbolEqualityComparer Default = new(); + + private SymbolEqualityComparer() { } + + public bool Equals(ISymbol? x, ISymbol? y) => x is null ? y is null : x.Equals(y); + + public int GetHashCode(ISymbol obj) => obj.GetHashCode(); +} +#endif diff --git a/src/BenchmarkDotNet.Analyzers/RequiredMemberAnalyzer.cs b/src/BenchmarkDotNet.Analyzers/RequiredMemberAnalyzer.cs new file mode 100644 index 0000000000..72b34d16a8 --- /dev/null +++ b/src/BenchmarkDotNet.Analyzers/RequiredMemberAnalyzer.cs @@ -0,0 +1,200 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using System.Collections.Immutable; +using System.Linq; + +namespace BenchmarkDotNet.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class RequiredMemberAnalyzer : DiagnosticAnalyzer +{ + internal static readonly DiagnosticDescriptor RequiredMemberCannotBeSetRule = new( + DiagnosticIds.General_BenchmarkClass_RequiredMemberCannotBeSet, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_RequiredMemberCannotBeSet_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_RequiredMemberCannotBeSet_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_RequiredMemberCannotBeSet_Description))); + + internal static readonly DiagnosticDescriptor ConstructorMustNotSetRequiredMembersRule = new( + DiagnosticIds.General_BenchmarkClass_ConstructorMustNotSetRequiredMembers, + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Title)), + AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_MessageFormat)), + "Usage", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: AnalyzerHelper.GetResourceString(nameof(BenchmarkDotNetAnalyzerResources.General_BenchmarkClass_ConstructorMustNotSetRequiredMembers_Description))); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(RequiredMemberCannotBeSetRule, ConstructorMustNotSetRequiredMembersRule); + + // Attributes whose members BDN sets when constructing the benchmark (via the object initializer or the + // cancellation-token initializer). A `required` member with any of these is satisfied at construction. + private static readonly string[] SettableMemberAttributeNames = + [ + "BenchmarkDotNet.Attributes.ParamsAttribute", + "BenchmarkDotNet.Attributes.ParamsSourceAttribute", + "BenchmarkDotNet.Attributes.ParamsAllValuesAttribute", + "BenchmarkDotNet.Attributes.BenchmarkCancellationAttribute", + ]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + +#if CODE_ANALYSIS_4_4 + // `required` members (and the IPropertySymbol/IFieldSymbol.IsRequired API) are C# 11 / Roslyn 4.4+. + context.RegisterCompilationStartAction(startContext => + { + var benchmarkAttribute = AnalyzerHelper.GetBenchmarkAttributeTypeSymbol(startContext.Compilation); + if (benchmarkAttribute == null) + { + return; + } + + var settableAttributes = SettableMemberAttributeNames + .Select(startContext.Compilation.GetTypeByMetadataName) + .Where(symbol => symbol != null) + .ToImmutableArray(); + var setsRequiredMembersAttribute = startContext.Compilation.GetTypeByMetadataName("System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute"); + + // The runnable derives from the benchmark type, so it must set every `required` member the type declares + // OR inherits. Each benchmark type checks its own base chain, stopping at a base that is itself a + // benchmark type (that one reports its own members), so no state is shared between types. + startContext.RegisterSymbolAction(symbolContext => + { + var benchmarkType = (INamedTypeSymbol)symbolContext.Symbol; + if (benchmarkType.TypeKind != TypeKind.Class || !IsBenchmarkType(benchmarkType, benchmarkAttribute)) + { + return; + } + + // The generated constructor chains to this one, so C# would force it to repeat [SetsRequiredMembers] + // (CS9039) - which suppresses required-member checking entirely and hides required members BDN + // cannot set. Report it rather than propagating the attribute into the generated code. + if (setsRequiredMembersAttribute != null) + { + foreach (var constructor in benchmarkType.InstanceConstructors) + { + if (constructor.Parameters.Length != 0 + || !constructor.GetAttributes().Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, setsRequiredMembersAttribute))) + { + continue; + } + + var constructorLocation = constructor.Locations.FirstOrDefault(candidate => candidate.IsInSource); + if (constructorLocation != null) + { + symbolContext.ReportDiagnostic(Diagnostic.Create(ConstructorMustNotSetRequiredMembersRule, constructorLocation, benchmarkType.Name)); + } + } + } + + Location? inheritedLocation = null; + + for (INamedTypeSymbol? current = benchmarkType; current != null && current.SpecialType != SpecialType.System_Object; current = current.BaseType) + { + bool declaredOnBenchmarkType = SymbolEqualityComparer.Default.Equals(current, benchmarkType); + + // A base that is itself a benchmark type reports its own (and its bases') required members at + // their declarations via its own walk, so stop here to avoid flagging them again. That walk only + // happens for a base declared in this compilation - one from a referenced assembly is never + // analyzed, so keep going and report its members at this type's base-type reference instead. + if (!declaredOnBenchmarkType + && IsBenchmarkType(current, benchmarkAttribute) + && current.DeclaringSyntaxReferences.Length > 0) + { + break; + } + + foreach (var member in current.GetMembers()) + { + bool isRequired = member switch + { + IPropertySymbol property => property.IsRequired, + IFieldSymbol field => field.IsRequired, + _ => false + }; + if (!isRequired) + { + continue; + } + + // BDN sets [Params*] members via the object initializer and an instance [BenchmarkCancellation] + // member via the cancellation-token initializer, so those required members are satisfied. + if (member.GetAttributes().Any(attribute => settableAttributes.Any(settable => AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, settable)))) + { + continue; + } + + // A member declared on the benchmark type is reported at its own declaration; one inherited + // from a base (source or a referenced assembly) is reported at the benchmark class's + // `: BaseType` reference - the base declaration doesn't know it's inherited by a benchmark, + // and that reference is always in the benchmark class's own source. + Location location = declaredOnBenchmarkType + ? member.Locations.FirstOrDefault(candidate => candidate.IsInSource) ?? Location.None + : inheritedLocation ??= GetBaseTypeReferenceLocation(benchmarkType); + + symbolContext.ReportDiagnostic(Diagnostic.Create(RequiredMemberCannotBeSetRule, location, member.Name)); + } + } + }, SymbolKind.NamedType); + }); +#endif + } + + private static bool IsBenchmarkType(INamedTypeSymbol type, INamedTypeSymbol benchmarkAttribute) + { + // The [Benchmark] method may be inherited, so walk the base types too. The attribute itself may also be + // a user's own deriving from [Benchmark], which is what the runtime resolves. + for (INamedTypeSymbol? current = type; current != null; current = current.BaseType) + { + if (current.GetMembers().OfType() + .Any(method => method.GetAttributes().Any(attribute => AnalyzerHelper.IsOrDerivesFrom(attribute.AttributeClass, benchmarkAttribute)))) + { + return true; + } + } + + return false; + } + + // The `: BaseType` reference in the benchmark class's declaration (its base class in the base list), where an + // inherited required member becomes the benchmark class's problem. Matched by name to avoid a semantic-model + // lookup (RS1030); the base class is always the first base-list entry, but a name match is robust to partials + // and interface-only base lists. Falls back to the type's own declaration. + private static Location GetBaseTypeReferenceLocation(INamedTypeSymbol benchmarkType) + { + var baseType = benchmarkType.BaseType; + if (baseType != null && baseType.SpecialType != SpecialType.System_Object) + { + foreach (var syntaxReference in benchmarkType.DeclaringSyntaxReferences) + { + if (syntaxReference.GetSyntax() is TypeDeclarationSyntax typeDeclaration && typeDeclaration.BaseList != null) + { + foreach (var baseTypeSyntax in typeDeclaration.BaseList.Types) + { + if (GetRightmostName(baseTypeSyntax.Type) == baseType.Name) + { + return baseTypeSyntax.GetLocation(); + } + } + } + } + } + + return benchmarkType.Locations.FirstOrDefault(candidate => candidate.IsInSource) ?? Location.None; + } + + private static string? GetRightmostName(TypeSyntax type) => type switch + { + IdentifierNameSyntax identifier => identifier.Identifier.ValueText, + GenericNameSyntax generic => generic.Identifier.ValueText, + QualifiedNameSyntax qualified => GetRightmostName(qualified.Right), + AliasQualifiedNameSyntax alias => GetRightmostName(alias.Name), + _ => null + }; +} diff --git a/src/BenchmarkDotNet.CodeFixers/BenchmarkDotNet.CodeFixers.csproj b/src/BenchmarkDotNet.CodeFixers/BenchmarkDotNet.CodeFixers.csproj index 2117f52ecf..557201c372 100644 --- a/src/BenchmarkDotNet.CodeFixers/BenchmarkDotNet.CodeFixers.csproj +++ b/src/BenchmarkDotNet.CodeFixers/BenchmarkDotNet.CodeFixers.csproj @@ -1,31 +1,14 @@ - + - netstandard2.0 - false BenchmarkDotNet.CodeFixers - true - $(NoWarn);CS1591 - 5.0 - bin\$(Configuration)\roslyn$(MccVersion)\cs - false - $(DefineConstants);CODE_ANALYSIS_3_8 - $(DefineConstants);CODE_ANALYSIS_4_8 - $(DefineConstants);CODE_ANALYSIS_5_0 - enable + - - - - - - - - + - + \ No newline at end of file diff --git a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs index 7d641b2357..e98f17201b 100644 --- a/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs +++ b/src/BenchmarkDotNet.Diagnostics.Windows/EtwProfiler.cs @@ -54,8 +54,16 @@ public EtwProfiler(EtwProfilerConfig config) public RunMode GetRunMode(BenchmarkCase benchmarkCase) => runMode; - public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) - => HardwareCounters.Validate(validationParameters, mandatory: false).ToAsyncEnumerable(); + // Iterated here rather than through BenchmarkDotNet's ToAsyncEnumerable polyfill: that one is internal to + // BenchmarkDotNet and compiled out of its .NET 10 asset, while this assembly is netstandard2.0 and would + // bind the netstandard asset's copy - which a .NET 10 host then cannot load. + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) + { + foreach (var error in HardwareCounters.Validate(validationParameters, mandatory: false)) + { + yield return error; + } + } public ValueTask HandleAsync(HostSignal signal, DiagnoserActionParameters parameters, CancellationToken cancellationToken) { diff --git a/src/BenchmarkDotNet/BenchmarkDotNet.csproj b/src/BenchmarkDotNet/BenchmarkDotNet.csproj index 07c28cb008..014d70872c 100644 --- a/src/BenchmarkDotNet/BenchmarkDotNet.csproj +++ b/src/BenchmarkDotNet/BenchmarkDotNet.csproj @@ -1,4 +1,4 @@ - + @@ -32,7 +32,6 @@ - diff --git a/src/BenchmarkDotNet/Code/ArrayParam.cs b/src/BenchmarkDotNet/Code/ArrayParam.cs deleted file mode 100644 index 548547f195..0000000000 --- a/src/BenchmarkDotNet/Code/ArrayParam.cs +++ /dev/null @@ -1,86 +0,0 @@ -using BenchmarkDotNet.Extensions; -using BenchmarkDotNet.Helpers; -using JetBrains.Annotations; -using System.Reflection; - -namespace BenchmarkDotNet.Code -{ - internal static class ArrayParam - { - private static (string BaseElementTypeRepr, string InnerDimensions) GetDisplayString(Type arrayType) - { - var elemType = arrayType.GetElementType()!; - - if (elemType.IsArray) - { - var (baseElementTypeRepr, innerDimensions) = GetDisplayString(elemType); - - return (baseElementTypeRepr, $"[{new string(',', arrayType.GetArrayRank() - 1)}]{innerDimensions}"); - } - - return (elemType.GetDisplayName(), $"[{new string(',', arrayType.GetArrayRank() - 1)}]"); - } - - public static string GetDisplayString(Array array) - { - string dimensionRepr = string.Join(", ", Enumerable.Range(0, array.Rank).Select(array.GetLength)); - - var (baseElementTypeRepr, innerDimensions) = GetDisplayString(array.GetType()); - - innerDimensions = string.Join("", innerDimensions.Split([']'], count: 2).Skip(1)); - - return $"{baseElementTypeRepr}[{dimensionRepr}]{innerDimensions}"; - } - } - - public class ArrayParam : IParam - { - private readonly T[] array; - private readonly Func? toSourceCode; - - private ArrayParam(T[] array, Func? toSourceCode = null) - { - this.array = array; - this.toSourceCode = toSourceCode; - } - - public object Value => array; - - public string DisplayText => ArrayParam.GetDisplayString(array); - - public string ToSourceCode() - => $"new {typeof(T).GetCorrectCSharpTypeName()}[] {{ {string.Join(", ", array.Select(item => toSourceCode?.Invoke(item) ?? SourceCodeHelper.ToSourceCode(item)))} }}"; - - /// - /// for types where calling .ToString() will be enough to re-create them in auto-generated source code file (integers, strings and other primitives) - /// - public static ArrayParam ForPrimitives(T[] array) => new ArrayParam(array); - - /// - /// for types where calling .ToString() will be NOT enough to re-create them in auto-generated source code file - /// - /// the array - /// method which transforms an item of type T to it's C# representation - /// example: point => $"new Point2d({point.X}, {point.Y})" - /// - [PublicAPI] public static ArrayParam ForComplexTypes(T[] array, Func toSourceCode) => new ArrayParam(array, toSourceCode); - - internal static IParam? FromObject(object array) - { - var type = array.GetType(); - if (!type.IsArray) - throw new InvalidOperationException("The argument must be an array"); - var elementType = type.GetElementType(); - if (elementType == null) - throw new InvalidOperationException("Failed to determine type of array elements"); - if (!SourceCodeHelper.IsCompilationTimeConstant(elementType)) - throw new InvalidOperationException("The argument must be an array of primitives"); - - var arrayParamType = typeof(ArrayParam<>).MakeGenericType(elementType); - - var methodInfo = arrayParamType.GetMethod(nameof(ForPrimitives), BindingFlags.Public | BindingFlags.Static) - ?? throw new InvalidOperationException($"{nameof(ForPrimitives)} not found"); - return (IParam?)methodInfo.Invoke(null, [array]); - } - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Code/CSharpParameterRenderer.cs b/src/BenchmarkDotNet/Code/CSharpParameterRenderer.cs new file mode 100644 index 0000000000..3d667171a4 --- /dev/null +++ b/src/BenchmarkDotNet/Code/CSharpParameterRenderer.cs @@ -0,0 +1,180 @@ +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Parameters; +using BenchmarkDotNet.Running; +using System.Reflection; + +namespace BenchmarkDotNet.Code; + +/// +/// Renders descriptions as C# expressions for the generated runnable. +/// All generated syntax lives here, so the parameters themselves stay language-neutral. +/// +internal sealed class CSharpParameterRenderer +{ + // The CancellationToken local declared at the top of the generated Run method. + private const string CancellationTokenLocalName = "cancellationToken"; + + // One entry per SourceRead reached through an instance member, in assignment order. The member is invoked in + // the constructor - the only place `base` is reachable - and the sequence passed out to Run, which extracts + // from it. Keyed by the read rather than by the parameter, so an arguments row invokes the member once; two + // [ParamsSource] members naming the same one still get a local each, because members range over their values + // independently and each read is its own object. Names are positional, so two sources sharing a simple name + // cannot collide. + private readonly IReadOnlyList<(SourceRead Read, string Local)> instanceReads; + + // One entry per SourceRead whose values are assigned by statement rather than by the object initializer - + // arguments and static [ParamsSource] members. A statement block can hold a local, so the read is extracted + // once into one and every parameter indexes into that. The initializer cannot, so a read used only there stays + // an inline expression; nothing is shared between members anyway. + private readonly IReadOnlyList<(SourceRead Read, string Local)> statementReads; + + private CSharpParameterRenderer( + IReadOnlyList<(SourceRead, string)> instanceReads, + IReadOnlyList<(SourceRead, string)> statementReads) + { + this.instanceReads = instanceReads; + this.statementReads = statementReads; + } + + public static CSharpParameterRenderer Create(BenchmarkCase benchmarkCase) + { + var instanceReads = new List<(SourceRead, string)>(); + var statementReads = new List<(SourceRead, string)>(); + + foreach (var parameter in benchmarkCase.Parameters.Items) + { + if (parameter.ParameterValue is not ParameterValue.FromSource fromSource) + continue; + + if (!IsStatic(fromSource.Read.Source)) + Reserve(instanceReads, fromSource.Read, "source"); + + // Everything the object initializer does not assign is assigned by a statement, which can hold a local. + if (parameter.IsArgument || parameter.IsStatic) + Reserve(statementReads, fromSource.Read, "read"); + } + + return new CSharpParameterRenderer(instanceReads, statementReads); + + static void Reserve(List<(SourceRead, string)> reads, SourceRead read, string prefix) + { + foreach (var reserved in reads) + { + if (ReferenceEquals(reserved.Item1, read)) + return; + } + + reads.Add((read, prefix + reads.Count)); + } + } + + /// `out IEnumerable<T> source0, ...` - the ctor's parameter list, and the `new` call's argument list. + /// `out T x` parses both as a parameter declaration and as an out-variable declaration expression, + /// so the same text serves the declaration and the call site. + public string RenderSourceOutParameters() + => string.Join(", ", instanceReads.Select(read => + $"out {ReturnType(read.Read.Source).GetCorrectCSharpTypeName()} {read.Local}")); + + /// `source0 = base.Values();` - the ctor body statements that read the instance sources. + public string RenderSourceCaptures() + => string.Join( + Environment.NewLine, + instanceReads.Select(read => $" {read.Local} = base.{read.Read.Source.Name}{InvocationPostfix(read.Read.Source)};")); + + /// `Element read0 = ...;` - one extraction per read, ahead of the statements that index into it. + public IEnumerable RenderStatementReads() + => statementReads.Select(read => $"{ElementTypeName(read.Read)} {read.Local} = {Extraction(read.Read)};"); + + public string Render(ParameterValue value) + { + switch (value) + { + case ParameterValue.Constant constant: + return SourceCodeHelper.ToSourceCode(constant.Value, constant.Type); + + case ParameterValue.FromSource fromSource: + { + // The value can't be embedded, so the child process re-obtains it by enumerating the source. + // GetParameterAsync returns the source's element type, so an element index binds directly. + string cast = $"({fromSource.TargetType.GetCorrectCSharpTypeName()})"; + string elementIndex = fromSource.ElementIndex is { } index ? $"[{index}]" : string.Empty; + + string source = StatementLocal(fromSource.Read) is { } local ? local : $"({Extraction(fromSource.Read)})"; + + return $"{cast}{source}{elementIndex}"; + } + + default: + throw new NotSupportedException($"{value.GetType().Name} is not a supported {nameof(ParameterValue)}."); + } + } + + // The generated code declares its locals with explicit, fully qualified types, as the template does. + private static string ElementTypeName(SourceRead read) + { + TryGetElementTypeName(read, out string name); + + return name; + } + + // False where the source does not name an element type. The extraction then has no type argument to bind + // against and does not compile, which is reported before any of this is emitted - `var` only keeps the + // declaration from adding a second error on top of that one. + private static bool TryGetElementTypeName(SourceRead read, out string name) + { + if (read.Source.GetSourceReturnType().TryGetSourceElementType(out var elementType)) + { + name = elementType.GetCorrectCSharpTypeName(); + return true; + } + + name = "var"; + return false; + } + + // Fully qualified (#778, #1007, #2821). + private string Extraction(SourceRead read) + { + string typeArgument = TryGetElementTypeName(read, out string elementTypeName) + ? $"<{elementTypeName}>" + : string.Empty; + + return $"await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(global::BenchmarkDotNet.Parameters.ParameterExtractor.GetParameterAsync{typeArgument}({SourceExpression(read)}, {read.ValueIndex}, {CancellationTokenLocalName}))"; + } + + private string? StatementLocal(SourceRead read) + { + foreach (var reserved in statementReads) + { + if (ReferenceEquals(reserved.Read, read)) + return reserved.Local; + } + + return null; + } + + // A static source is called where the value is assigned; an instance source is read into a local in the + // constructor, because `base` is not reachable from Run's object initializer. + private string SourceExpression(SourceRead read) + { + if (IsStatic(read.Source)) + return $"{read.Source.DeclaringType!.GetCorrectCSharpTypeName()}.{read.Source.Name}{InvocationPostfix(read.Source)}"; + + foreach (var reserved in instanceReads) + { + if (ReferenceEquals(reserved.Read, read)) + return reserved.Local; + } + + throw new InvalidOperationException($"No local was reserved for the value {read.Source.Name} provides."); + } + + private static string InvocationPostfix(MemberInfo source) => source is PropertyInfo ? string.Empty : "()"; + + private static bool IsStatic(MemberInfo source) + => source is PropertyInfo property ? property.GetMethod!.IsStatic : ((MethodInfo) source).IsStatic; + + private static Type ReturnType(MemberInfo source) + => source is PropertyInfo property ? property.GetMethod!.ReturnType : ((MethodInfo) source).ReturnType; +} diff --git a/src/BenchmarkDotNet/Code/CodeGenerator.cs b/src/BenchmarkDotNet/Code/CodeGenerator.cs index c575d01713..708495ab32 100644 --- a/src/BenchmarkDotNet/Code/CodeGenerator.cs +++ b/src/BenchmarkDotNet/Code/CodeGenerator.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Characteristics; using BenchmarkDotNet.Diagnosers; -using BenchmarkDotNet.Disassemblers; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; @@ -29,19 +28,24 @@ internal static async ValueTask GenerateAsync(BuildPartition buildPartit var declarationsProvider = GetDeclarationsProvider(benchmark); var extraFields = declarationsProvider.GetExtraFields(); + var parameterRenderer = CSharpParameterRenderer.Create(benchmark); string benchmarkTypeCode = declarationsProvider .ReplaceTemplate(new SmartStringBuilder(benchmarkTypeTemplate)) .Replace("$ID$", buildInfo.Id.ToString()) .Replace("$JobSetDefinition$", GetJobsSetDefinition(benchmark)) - .Replace("$ParamsContent$", GetParamsContent(benchmark)) + .Replace("$SourceOutParameters$", parameterRenderer.RenderSourceOutParameters()) + .Replace("$SourceOutArguments$", parameterRenderer.RenderSourceOutParameters()) + .Replace("$SourceCaptures$", parameterRenderer.RenderSourceCaptures()) + .Replace("$ParamsInitializer$", GetParamsInitializer(benchmark, parameterRenderer)) .Replace("$CancellationTokenAssignment$", GetCancellationTokenAssignment(benchmark)) + .Replace("$CancellationTokenInitializer$", GetCancellationTokenInitializer(benchmark)) .Replace("$ArgumentsDefinition$", GetArgumentsDefinition(benchmark)) .Replace("$DeclareFieldsContainer$", GetDeclareFieldsContainer(benchmark, buildInfo.Id, extraFields)) - .Replace("$InitializeArgumentFields$", GetInitializeArgumentFields(benchmark)) + .Replace("$StaticParamsAndArgsContent$", GetStaticParamsAndArgsContent(benchmark, parameterRenderer)) .Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark)) .Replace("$RunExtraIteration$", buildInfo.Config.HasExtraIterationDiagnoser(benchmark) ? "true" : "false") - .Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName) + .Replace("$DisassemblerEntryMethodName$", RunnableConstants.ForDisassemblyDiagnoserMethodName) .Replace("$InProcessDiagnoserRouters$", GetInProcessDiagnoserRouters(buildInfo)) .ToString(); @@ -137,46 +141,96 @@ private static DeclarationsProvider GetDeclarationsProvider(BenchmarkCase benchm } // internal for tests + internal static string GetParamsInitializer(BenchmarkCase benchmarkCase) + => GetParamsInitializer(benchmarkCase, CSharpParameterRenderer.Create(benchmarkCase)); - internal static string GetParamsContent(BenchmarkCase benchmarkCase) + private static string GetParamsInitializer(BenchmarkCase benchmarkCase, CSharpParameterRenderer renderer) => string.Join( - string.Empty, + $",{Environment.NewLine} ", benchmarkCase.Parameters.Items - .Where(parameter => !parameter.IsArgument) - .Select(parameter => $"{(parameter.IsStatic ? benchmarkCase.Descriptor.Type.GetCorrectCSharpTypeName() : "base")}.{parameter.Name} = {parameter.ToSourceCode()};")); + .Where(parameter => !parameter.IsArgument && !parameter.IsStatic) + .Select(parameter => $"{parameter.Name} = {renderer.Render(parameter.ParameterValue)}")); + // Static [BenchmarkCancellation] members only - instance members are set by the object initializer + // (GetCancellationTokenInitializer), so emitting them here too would assign them twice. internal static string GetCancellationTokenAssignment(BenchmarkCase benchmarkCase) { var targetType = benchmarkCase.Descriptor.Type; - var cancellationTokenMembers = new System.Collections.Generic.List(); + List cancellationTokenMembers = []; var typeFullName = targetType.GetCorrectCSharpTypeName(); + // As in GetCancellationTokenInitializer: one entry per name. Here a repeat compiles, since these are + // statements, but `Type.Name` binds to the most derived member both times - so it would assign that + // one twice and the member hiding it never. + HashSet emitted = new(StringComparer.Ordinal); + + // FlattenHierarchy reaches a base type's statics, which reflection otherwise withholds - the same set + // BenchmarkCancellationValidator reports on, so it cannot accept a member this never assigns. // Check properties - foreach (var property in targetType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static)) + foreach (var property in targetType.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) { - if (property.PropertyType == typeof(System.Threading.CancellationToken) && + if (property.PropertyType == typeof(CancellationToken) && property.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false) && property.CanWrite && - property.GetSetMethod() is { } setter) + property.GetSetMethod() is { IsStatic: true } && + emitted.Add(property.Name)) { - var target = setter.IsStatic ? typeFullName : "base"; - cancellationTokenMembers.Add($" {target}.{property.Name} = cancellationToken;"); + cancellationTokenMembers.Add($" {typeFullName}.{property.Name} = cancellationToken;"); } } // Check fields - foreach (var field in targetType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static)) + foreach (var field in targetType.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)) { - if (field.FieldType == typeof(System.Threading.CancellationToken) && - field.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false)) + if (field.FieldType == typeof(CancellationToken) && + field.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false) && + emitted.Add(field.Name)) { - var target = field.IsStatic ? typeFullName : "base"; - cancellationTokenMembers.Add($" {target}.{field.Name} = cancellationToken;"); + cancellationTokenMembers.Add($" {typeFullName}.{field.Name} = cancellationToken;"); } } return cancellationTokenMembers.Count > 0 - ? string.Join(System.Environment.NewLine, cancellationTokenMembers) + System.Environment.NewLine + ? string.Join(Environment.NewLine, cancellationTokenMembers) + Environment.NewLine + : string.Empty; + } + + private static string GetCancellationTokenInitializer(BenchmarkCase benchmarkCase) + { + var targetType = benchmarkCase.Descriptor.Type; + List entries = []; + + // One entry per name. GetFields hands back a hidden base field alongside the `new` one that hides it - + // GetProperties does not, which is why only fields reach this - and the same name twice in an object + // initializer is CS1912. The name binds to the most derived member either way, so the second entry + // could only ever repeat the first. + HashSet emitted = new(StringComparer.Ordinal); + + foreach (var property in targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.PropertyType == typeof(CancellationToken) && + property.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false) && + property.CanWrite && + property.GetSetMethod() is { IsStatic: false } && + emitted.Add(property.Name)) + { + entries.Add($"{property.Name} = cancellationToken,"); + } + } + + foreach (var field in targetType.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + if (field.FieldType == typeof(CancellationToken) && + field.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false) && + !field.IsStatic && + emitted.Add(field.Name)) + { + entries.Add($"{field.Name} = cancellationToken,"); + } + } + + return entries.Count > 0 + ? string.Join($"{Environment.NewLine} ", entries) : string.Empty; } @@ -184,12 +238,12 @@ private static string GetArgumentsDefinition(BenchmarkCase benchmarkCase) => string.Join( ", ", benchmarkCase.Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index}")); + .Select((parameter, index) => $"{GetParameterModifier(parameter)} {parameter.ParameterType.GetCorrectCSharpTypeName()} {RunnableConstants.ArgParamPrefix}{index}")); private static string GetDeclareFieldsContainer(BenchmarkCase benchmarkCase, BenchmarkId benchmarkId, string[] extraFields) { var fields = benchmarkCase.Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"public {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name!)).GetCorrectCSharpTypeName()} argField{index};") + .Select((parameter, index) => $"public {GetFieldType(parameter.ParameterType, benchmarkCase.Parameters.GetArgument(parameter.Name!)).GetCorrectCSharpTypeName()} {RunnableConstants.ArgFieldPrefix}{index};") .Concat(extraFields) .ToArray(); @@ -200,9 +254,9 @@ private static string GetDeclareFieldsContainer(BenchmarkCase benchmarkCase, Ben } var sb = new StringBuilder(); - sb.AppendLine(""" + sb.AppendLine($$""" [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Auto)] - private struct FieldsContainer + private struct {{RunnableConstants.FieldsContainerTypeName}} { """); foreach (var field in fields) @@ -211,28 +265,38 @@ private struct FieldsContainer } sb.AppendLine(" }"); sb.AppendLine(); - sb.AppendLine($" private global::BenchmarkDotNet.Autogenerated.Runnable_{benchmarkId.Value}.FieldsContainer __fieldsContainer;"); + sb.AppendLine($" private global::{RunnableConstants.EmittedTypePrefix}{benchmarkId.Value}.{RunnableConstants.FieldsContainerTypeName} {RunnableConstants.FieldsContainerName};"); return sb.ToString(); } /* - + [global::System.Runtime.InteropServices.StructLayout(global::System.Runtime.InteropServices.LayoutKind.Auto)] - private unsafe struct FieldsContainer + private unsafe struct __FieldsContainer { $DeclareArgumentFields$ $ExtraFields$ } - private global::BenchmarkDotNet.Autogenerated.Runnable_$ID$.FieldsContainer __fieldsContainer; - + private global::BenchmarkDotNet.Autogenerated.Runnable_$ID$.__FieldsContainer __fieldsContainer; + */ - private static string GetInitializeArgumentFields(BenchmarkCase benchmarkCase) - => string.Join( - Environment.NewLine, - benchmarkCase.Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"this.__fieldsContainer.argField{index} = {benchmarkCase.Parameters.GetArgument(parameter.Name!).ToSourceCode()};")); // we init the fields in ctor to provoke all possible allocations and overhead of other type + // Assigned after the instance is created: argument fields live on it, and a static parameter may draw its + // value from an instance source, which the constructor captures. + private static string GetStaticParamsAndArgsContent(BenchmarkCase benchmarkCase, CSharpParameterRenderer renderer) + { + var staticParams = benchmarkCase.Parameters.Items + .Where(parameter => !parameter.IsArgument && parameter.IsStatic) + .Select(parameter => $"{benchmarkCase.Descriptor.Type.GetCorrectCSharpTypeName()}.{parameter.Name} = {renderer.Render(parameter.ParameterValue)};"); + + var argumentFields = benchmarkCase.Descriptor.WorkloadMethod.GetParameters() + .Select((parameter, index) => $"instance.{RunnableConstants.FieldsContainerName}.{RunnableConstants.ArgFieldPrefix}{index} = {renderer.Render(benchmarkCase.Parameters.GetArgument(parameter.Name!).ParameterValue)};"); + + return string.Join( + $"{Environment.NewLine} ", + renderer.RenderStatementReads().Concat(staticParams).Concat(argumentFields)); + } private static string GetEngineFactoryTypeName(BenchmarkCase benchmarkCase) { @@ -407,10 +471,10 @@ private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGen if (runCallType == CodeGenBenchmarkRunCallType.Reflection) { // Use reflection to call benchmark's Run method indirectly. - return """ + return $$""" await ((global::System.Threading.Tasks.ValueTask) typeof(global::BenchmarkDotNet.Autogenerated.UniqueProgramName).Assembly - .GetType($"BenchmarkDotNet.Autogenerated.Runnable_{id}") - .GetMethod("Run", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static) + .GetType($"{{RunnableConstants.EmittedTypePrefix}}{id}") + .GetMethod("{{RunnableConstants.RunMethodName}}", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.Static) .Invoke(null, new global::System.Object[] { host, benchmarkName, diagnoserRunMode })) .ConfigureAwait(false); """; @@ -422,7 +486,7 @@ private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGen foreach (var buildInfo in buildPartition.Benchmarks) { - @switch.AppendLine($"case {buildInfo.Id.Value}: await BenchmarkDotNet.Autogenerated.Runnable_{buildInfo.Id.Value}.Run(host, benchmarkName, diagnoserRunMode); break;"); + @switch.AppendLine($"case {buildInfo.Id.Value}: await {RunnableConstants.EmittedTypePrefix}{buildInfo.Id.Value}.{RunnableConstants.RunMethodName}(host, benchmarkName, diagnoserRunMode); break;"); } @switch.AppendLine("default: throw new System.NotSupportedException(\"invalid benchmark id\");"); @@ -433,9 +497,9 @@ private static string GetBenchmarkRunCall(BuildPartition buildPartition, CodeGen private static Type GetFieldType(Type argumentType, ParameterInstance argument) { - // #774 we can't store Span in a field, so we store an array (which is later casted to Span when we load the arguments) - if (argumentType.IsStackOnlyWithImplicitCast(argument.Value)) - return argument.Value.GetType(); + // #774 we can't store ByRefLike in a field, so we store what the value is cast to (which is later converted back to the ByRefLike when we load the arguments). + if (argumentType.WithoutRefModifier().IsByRefLike() && argument.Value is { } value) + return value.GetType(); return argumentType; } diff --git a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs index 155fba85ee..03490585fe 100644 --- a/src/BenchmarkDotNet/Code/DeclarationsProvider.cs +++ b/src/BenchmarkDotNet/Code/DeclarationsProvider.cs @@ -99,7 +99,7 @@ protected string GetLoadArguments() .Select((parameter, index) => { var refModifier = parameter.ParameterType.IsByRef ? "ref" : string.Empty; - return $"{refModifier} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {refModifier} this.__fieldsContainer.argField{index};"; + return $"{refModifier} {parameter.ParameterType.GetCorrectCSharpTypeName()} arg{index} = {refModifier} this.{RunnableConstants.FieldsContainerName}.{RunnableConstants.ArgFieldPrefix}{index};"; }) ); @@ -110,7 +110,7 @@ protected string GetPassArguments() .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} arg{index}") ); - // Renders the benchmark method's parameter types as a Type[] for __ResolveWorkloadMethods to match overloads + // Renders the benchmark method's parameter types as a Type[] for Run's workload-method resolution to match overloads // exactly. Each is a typeof(...) of the element type, re-wrapping by-ref/pointer via reflection (typeof can't // express `T&`), so resolution never has to name the method's (possibly unspellable) return type. private string GetWorkloadMethodParameterTypes() @@ -134,7 +134,7 @@ protected string GetPassArgumentsDirect() => string.Join( ", ", Descriptor.WorkloadMethod.GetParameters() - .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} this.__fieldsContainer.argField{index}") + .Select((parameter, index) => $"{CodeGenerator.GetParameterModifier(parameter)} this.{RunnableConstants.FieldsContainerName}.{RunnableConstants.ArgFieldPrefix}{index}") ); } @@ -148,7 +148,7 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString string passArguments = GetPassArguments(); string workloadMethodCall = GetWorkloadMethodCall(passArguments); string coreImpl = $$""" - private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.OverheadActionUnrollMethodName}}({{CoreParameters}}) { unsafe { @@ -156,13 +156,13 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString {{StartClockSyncCode}} while (--invokeCount >= 0) { - this.__Overhead({{passArguments}});@Unroll@ + this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}});@Unroll@ } {{ReturnSyncCode}} } } - private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.OverheadActionNoUnrollMethodName}}({{CoreParameters}}) { unsafe { @@ -170,13 +170,13 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString {{StartClockSyncCode}} while (--invokeCount >= 0) { - this.__Overhead({{passArguments}}); + this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}}); } {{ReturnSyncCode}} } } - private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.WorkloadActionUnrollMethodName}}({{CoreParameters}}) { unsafe { @@ -190,7 +190,7 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString } } - private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.WorkloadActionNoUnrollMethodName}}({{CoreParameters}}) { unsafe { @@ -223,29 +223,29 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString string passArguments = GetPassArguments(); string workloadMethodCall = $"global::{typeof(AwaitHelper).FullName}.{nameof(AwaitHelper.GetResult)}({GetWorkloadMethodCall(passArguments).TrimEnd(';')});"; string coreImpl = $$""" - private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.OverheadActionUnrollMethodName}}({{CoreParameters}}) { {{loadArguments}} {{StartClockSyncCode}} while (--invokeCount >= 0) { - this.__Overhead({{passArguments}});@Unroll@ + this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}});@Unroll@ } {{ReturnSyncCode}} } - private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.OverheadActionNoUnrollMethodName}}({{CoreParameters}}) { {{loadArguments}} {{StartClockSyncCode}} while (--invokeCount >= 0) { - this.__Overhead({{passArguments}}); + this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}}); } {{ReturnSyncCode}} } - private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.WorkloadActionUnrollMethodName}}({{CoreParameters}}) { {{loadArguments}} {{StartClockSyncCode}} @@ -256,7 +256,7 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString {{ReturnSyncCode}} } - private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.WorkloadActionNoUnrollMethodName}}({{CoreParameters}}) { {{loadArguments}} {{StartClockSyncCode}} @@ -282,19 +282,19 @@ internal abstract class AsyncDeclarationsProviderBase(BenchmarkCase benchmark) : public override string[] GetExtraFields() => [ - $"public {typeof(WorkloadValueTaskSource).GetCorrectCSharpTypeName()} workloadValueTaskSource;", - $"public {typeof(IClock).GetCorrectCSharpTypeName()} clock;", - "public long invokeCount;" + $"public {typeof(WorkloadValueTaskSource).GetCorrectCSharpTypeName()} {RunnableConstants.WorkloadValueTaskSourceFieldName};", + $"public {typeof(IClock).GetCorrectCSharpTypeName()} {RunnableConstants.ClockFieldName};", + $"public long {RunnableConstants.InvokeCountFieldName};" ]; protected override string GetExtraGlobalSetupImpl() => $$""" - this.__fieldsContainer.workloadValueTaskSource = new {{typeof(WorkloadValueTaskSource).GetCorrectCSharpTypeName()}}(); - this.__StartWorkload(); + this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}} = new {{typeof(WorkloadValueTaskSource).GetCorrectCSharpTypeName()}}(); + this.{{RunnableConstants.StartWorkloadMethodName}}(); """; protected override string GetExtraGlobalCleanupImpl() - => "this.__fieldsContainer.workloadValueTaskSource.Complete();"; + => $"this.{RunnableConstants.FieldsContainerName}.{RunnableConstants.WorkloadValueTaskSourceFieldName}.Complete();"; protected bool TryGetAsyncMethodBuilderAttribute(out string asyncMethodBuilderAttribute) { @@ -355,57 +355,57 @@ protected override SmartStringBuilder ReplaceCore(SmartStringBuilder smartString Type workloadCoreReturnType = GetWorkloadCoreReturnType(hasAsyncMethodBuilderAttribute, WorkloadAwaitableReturnType); string finalReturn = GetFinalReturn(workloadCoreReturnType); string coreImpl = $$""" - private {{CoreReturnType}} OverheadActionUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.OverheadActionUnrollMethodName}}({{CoreParameters}}) { - return this.OverheadActionNoUnroll(invokeCount * {{unrollFactor}}, clock); + return this.{{RunnableConstants.OverheadActionNoUnrollMethodName}}(invokeCount * {{unrollFactor}}, clock); } - private {{CoreReturnType}} OverheadActionNoUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.OverheadActionNoUnrollMethodName}}({{CoreParameters}}) { {{StartClockSyncCode}} while (--invokeCount >= 0) { - this.__Overhead({{passArguments}}); + this.{{RunnableConstants.OverheadImplementationMethodName}}({{passArguments}}); } {{ReturnSyncCode}} } - private {{CoreReturnType}} WorkloadActionUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.WorkloadActionUnrollMethodName}}({{CoreParameters}}) { - return this.WorkloadActionNoUnroll(invokeCount * {{unrollFactor}}, clock); + return this.{{RunnableConstants.WorkloadActionNoUnrollMethodName}}(invokeCount * {{unrollFactor}}, clock); } - private {{CoreReturnType}} WorkloadActionNoUnroll({{CoreParameters}}) + private {{CoreReturnType}} {{RunnableConstants.WorkloadActionNoUnrollMethodName}}({{CoreParameters}}) { - this.__fieldsContainer.invokeCount = invokeCount; - this.__fieldsContainer.clock = clock; + this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.InvokeCountFieldName}} = invokeCount; + this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.ClockFieldName}} = clock; // The source is allocated and the workload loop started in __GlobalSetup, // so this hot path is branchless and allocation-free. - return this.__fieldsContainer.workloadValueTaskSource.Continue(); + return this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}}.Continue(); } - private async void __StartWorkload() + private async void {{RunnableConstants.StartWorkloadMethodName}}() { - await __WorkloadCore(); + await {{RunnableConstants.WorkloadCoreMethodName}}(); } - + {{asyncMethodBuilderAttribute}} - private async {{workloadCoreReturnType.GetCorrectCSharpTypeName()}} __WorkloadCore() + private async {{workloadCoreReturnType.GetCorrectCSharpTypeName()}} {{RunnableConstants.WorkloadCoreMethodName}}() { try { - if (await this.__fieldsContainer.workloadValueTaskSource.GetIsComplete()) + if (await this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}}.GetIsComplete()) { {{finalReturn}} } while (true) { - {{typeof(StartedClock).GetCorrectCSharpTypeName()}} startedClock = {{typeof(ClockExtensions).GetCorrectCSharpTypeName()}}.Start(this.__fieldsContainer.clock); - while (--this.__fieldsContainer.invokeCount >= 0) + {{typeof(StartedClock).GetCorrectCSharpTypeName()}} startedClock = {{typeof(ClockExtensions).GetCorrectCSharpTypeName()}}.Start(this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.ClockFieldName}}); + while (--this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.InvokeCountFieldName}} >= 0) { {{GetCallAndConsumeImpl(workloadMethodCall)}} } - if (await this.__fieldsContainer.workloadValueTaskSource.SetResultAndGetIsComplete(startedClock.GetElapsed())) + if (await this.{{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}}.SetResultAndGetIsComplete(startedClock.GetElapsed())) { {{finalReturn}} } @@ -413,7 +413,7 @@ private async void __StartWorkload() } catch (global::System.Exception e) { - __fieldsContainer.workloadValueTaskSource.SetException(e); + {{RunnableConstants.FieldsContainerName}}.{{RunnableConstants.WorkloadValueTaskSourceFieldName}}.SetException(e); {{finalReturn}} } } @@ -457,4 +457,4 @@ protected override string GetCallAndConsumeImpl(string workloadMethodCall) """; } } -} \ No newline at end of file +} diff --git a/src/BenchmarkDotNet/Code/EnumParam.cs b/src/BenchmarkDotNet/Code/EnumParam.cs deleted file mode 100644 index 976a8fd25c..0000000000 --- a/src/BenchmarkDotNet/Code/EnumParam.cs +++ /dev/null @@ -1,60 +0,0 @@ -using BenchmarkDotNet.Extensions; -using System.Globalization; - -namespace BenchmarkDotNet.Code -{ - public class EnumParam : IParam - { - // Preserves type information for enum values from F# code - // See also: - // https://github.com/dotnet/fsharp/issues/995 - private readonly Type type; - - private EnumParam(object value, Type type) - { - this.Value = value; - this.type = type; - } - - public object Value { get; } - - public string DisplayText => $"{Enum.ToObject(type, Value)}"; - - public string ToSourceCode() => - $"({type.GetCorrectCSharpTypeName()})({ToInvariantCultureString()})"; - - internal static IParam FromObject(object value, Type? type = null) - { - type = type ?? value.GetType(); - if (!type.IsEnum) - throw new ArgumentOutOfRangeException(nameof(type)); - - return new EnumParam(value, type); - } - - private string ToInvariantCultureString() - { - switch (Type.GetTypeCode(Enum.GetUnderlyingType(type))) - { - case TypeCode.Byte: - return ((byte)Value).ToString(CultureInfo.InvariantCulture); - case TypeCode.Int16: - return ((short)Value).ToString(CultureInfo.InvariantCulture); - case TypeCode.Int32: - return ((int)Value).ToString(CultureInfo.InvariantCulture); - case TypeCode.Int64: - return ((long)Value).ToString(CultureInfo.InvariantCulture); - case TypeCode.SByte: - return ((sbyte)Value).ToString(CultureInfo.InvariantCulture); - case TypeCode.UInt16: - return ((ushort)Value).ToString(CultureInfo.InvariantCulture); - case TypeCode.UInt32: - return ((uint)Value).ToString(CultureInfo.InvariantCulture); - case TypeCode.UInt64: - return ((ulong)Value).ToString(CultureInfo.InvariantCulture); - default: - throw new ArgumentOutOfRangeException(nameof(Value)); - } - } - } -} diff --git a/src/BenchmarkDotNet/Code/IParam.cs b/src/BenchmarkDotNet/Code/IParam.cs deleted file mode 100644 index eb094b35bd..0000000000 --- a/src/BenchmarkDotNet/Code/IParam.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace BenchmarkDotNet.Code -{ - public interface IParam - { - /// - /// value of the parameter object for benchmark - /// used internally (e.g. by the InProcessToolchain) - /// - object Value { get; } - - /// - /// used to display the value (e.g. in summary in Params column) - /// - string DisplayText { get; } - - /// - /// this source code is used to create parameter for benchmark - /// in C# source code file - /// example: $"new Point2D({Value.X}, {Value.Y})" - /// - string ToSourceCode(); - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Code/RunnableConstants.cs b/src/BenchmarkDotNet/Code/RunnableConstants.cs new file mode 100644 index 0000000000..ea434ce6c0 --- /dev/null +++ b/src/BenchmarkDotNet/Code/RunnableConstants.cs @@ -0,0 +1,63 @@ +using System.Collections.Immutable; + +namespace BenchmarkDotNet.Code; + +// Member names emitted by the code generator onto the generated runnable type - used by both the out-of-process +// template (Templates/BenchmarkType.txt via CodeGenerator/DeclarationsProvider) and the in-process IL emitter +// (RunnableEmitter). ReservedInstanceMemberNames (below) reuses these so the reserved-name checks can't drift. +internal class RunnableConstants +{ + public const string EmittedTypePrefix = "BenchmarkDotNet.Autogenerated.Runnable_"; + public const string ArgFieldPrefix = "argField"; + public const string ArgParamPrefix = "arg"; + + public const string InvokeCountParamName = "invokeCount"; + public const string ClockParamName = "clock"; + + public const string RunMethodName = "__Run"; + public const string FieldsContainerTypeName = "__FieldsContainer"; + public const string FieldsContainerName = "__fieldsContainer"; + public const string WorkloadValueTaskSourceFieldName = "workloadValueTaskSource"; + public const string ClockFieldName = "clock"; + public const string InvokeCountFieldName = "invokeCount"; + + public const string TrickTheJitCoreMethodName = "__TrickTheJIT__"; + public const string ForDisassemblyDiagnoserMethodName = "__ForDisassemblyDiagnoser__"; + public const string GlobalSetupMethodName = "__GlobalSetup"; + public const string GlobalCleanupMethodName = "__GlobalCleanup"; + public const string IterationSetupMethodName = "__IterationSetup"; + public const string IterationCleanupMethodName = "__IterationCleanup"; + public const string OverheadImplementationMethodName = "__Overhead"; + public const string OverheadActionUnrollMethodName = "__OverheadActionUnroll"; + public const string OverheadActionNoUnrollMethodName = "__OverheadActionNoUnroll"; + public const string WorkloadActionUnrollMethodName = "__WorkloadActionUnroll"; + public const string WorkloadActionNoUnrollMethodName = "__WorkloadActionNoUnroll"; + public const string StartWorkloadMethodName = "__StartWorkload"; + public const string WorkloadCoreMethodName = "__WorkloadCore"; + + // The constants above that name members the generated runnable declares. A [Params]/[ParamsSource]/ + // [ParamsAllValues] member with one of these names is shadowed by the generated member and makes the generated + // program fail to compile (CS1913 in the object initializer, or an inaccessible-member error). Shared by + // ParamsValidator (runtime) and the reserved-name analyzer (compile time) via a linked file. + // NOTE: ImmutableHashSet.Create (not a collection expression) so this linked file also compiles against the + // older System.Collections.Immutable used by the analyzer's Roslyn targets. + public static readonly ImmutableHashSet ReservedInstanceMemberNames = ImmutableHashSet.Create(StringComparer.Ordinal, + [ + RunMethodName, + FieldsContainerTypeName, + FieldsContainerName, + TrickTheJitCoreMethodName, + OverheadImplementationMethodName, + OverheadActionUnrollMethodName, + OverheadActionNoUnrollMethodName, + WorkloadActionUnrollMethodName, + WorkloadActionNoUnrollMethodName, + ForDisassemblyDiagnoserMethodName, + GlobalSetupMethodName, + GlobalCleanupMethodName, + IterationSetupMethodName, + IterationCleanupMethodName, + StartWorkloadMethodName, + WorkloadCoreMethodName, + ]); +} diff --git a/src/BenchmarkDotNet/Configs/DefaultConfig.cs b/src/BenchmarkDotNet/Configs/DefaultConfig.cs index df47aeb52f..d6a752e8e2 100644 --- a/src/BenchmarkDotNet/Configs/DefaultConfig.cs +++ b/src/BenchmarkDotNet/Configs/DefaultConfig.cs @@ -79,6 +79,8 @@ public IEnumerable GetValidators() yield return DeferredExecutionValidator.FailOnError; yield return ParamsAllValuesValidator.FailOnError; yield return ParamsValidator.FailOnError; + yield return RequiredMemberValidator.FailOnError; + yield return SourceReturnTypeValidator.FailOnError; yield return BenchmarkCancellationValidator.FailOnError; yield return RuntimeValidator.DontFailOnError; } diff --git a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs index 18c1b65fd6..7e02e15c64 100644 --- a/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs +++ b/src/BenchmarkDotNet/Configs/ImmutableConfigBuilder.cs @@ -30,6 +30,8 @@ public static class ImmutableConfigBuilder DeferredExecutionValidator.DontFailOnError, ParamsAllValuesValidator.FailOnError, ParamsValidator.FailOnError, + RequiredMemberValidator.FailOnError, + SourceReturnTypeValidator.FailOnError, BenchmarkCancellationValidator.FailOnError }; diff --git a/src/BenchmarkDotNet/ConsoleArguments/CorrectionsSuggester.cs b/src/BenchmarkDotNet/ConsoleArguments/CorrectionsSuggester.cs index 73d5c68414..a922903ec9 100644 --- a/src/BenchmarkDotNet/ConsoleArguments/CorrectionsSuggester.cs +++ b/src/BenchmarkDotNet/ConsoleArguments/CorrectionsSuggester.cs @@ -1,20 +1,30 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Running; namespace BenchmarkDotNet.ConsoleArguments { - public class CorrectionsSuggester + internal sealed class CorrectionsSuggester { // note This is a heuristic value, we suppose that user can make three or fewer typos. private static int PossibleTyposCount => 3; private readonly HashSet possibleBenchmarkNameFilters = []; private readonly HashSet actualFullBenchmarkNames = []; - public CorrectionsSuggester(IReadOnlyList types) + internal CorrectionsSuggester(IReadOnlyList types) + => Populate(TypeFilter.Filter(DefaultConfig.Instance, types)); + + private CorrectionsSuggester(BenchmarkRunInfo[] benchmarkRunInfos) + => Populate(benchmarkRunInfos); + + internal static async ValueTask CreateAsync(IReadOnlyList types, CancellationToken cancellationToken = default) + => new(await TypeFilter.FilterAsync(DefaultConfig.Instance, types, cancellationToken).ConfigureAwait()); + + private void Populate(BenchmarkRunInfo[] benchmarkRunInfos) { - foreach (var benchmarkRunInfo in TypeFilter.Filter(DefaultConfig.Instance, types)) + foreach (var benchmarkRunInfo in benchmarkRunInfos) { foreach (var benchmarkCase in benchmarkRunInfo.BenchmarksCases) { diff --git a/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs b/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs index 31b82a11d8..2ef7487f56 100644 --- a/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs +++ b/src/BenchmarkDotNet/Diagnosers/CompositeDiagnoser.cs @@ -11,6 +11,7 @@ using System.Collections.Concurrent; using System.Collections.Immutable; using System.ComponentModel; +using System.Runtime.CompilerServices; namespace BenchmarkDotNet.Diagnosers { @@ -55,8 +56,22 @@ public void DisplayResults(ILogger logger) } } + // Written out rather than composed with async LINQ - see CompositeValidator.ValidateAsync for why. public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) - => diagnosers.ToAsyncEnumerable().SelectMany(diagnoser => diagnoser.ValidateAsync(validationParameters)); + => ValidateAsyncCore(validationParameters); + + private async IAsyncEnumerable ValidateAsyncCore(ValidationParameters validationParameters, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var diagnoser in diagnosers) + { +#pragma warning disable CA2007 + await foreach (var error in diagnoser.ValidateAsync(validationParameters).ConfigureAwait(cancellationToken)) +#pragma warning restore CA2007 + { + yield return error; + } + } + } } public sealed class CompositeInProcessDiagnoser(IReadOnlyList inProcessDiagnosers) @@ -123,4 +138,4 @@ public async ValueTask HandleAsync(BenchmarkSignal signal, CancellationToken can } } } -} \ No newline at end of file +} diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs index fadec8ca18..9f90a881c4 100644 --- a/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs +++ b/src/BenchmarkDotNet/Disassemblers/ClrMdArgs.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Code; using System.Text.Json.Serialization; namespace BenchmarkDotNet.Disassemblers @@ -17,7 +18,7 @@ internal struct ClrMdArgs(int processId, string typeName, string methodName, boo internal bool PrintSource = printSource; [JsonInclude] - internal int MaxDepth = methodName == DisassemblerConstants.DisassemblerEntryMethodName && maxDepth != int.MaxValue ? maxDepth + 1 : maxDepth; + internal int MaxDepth = methodName == RunnableConstants.ForDisassemblyDiagnoserMethodName && maxDepth != int.MaxValue ? maxDepth + 1 : maxDepth; [JsonInclude] internal string[] Filters = filters; diff --git a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs index cc54ddea0c..92309a4be0 100644 --- a/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs +++ b/src/BenchmarkDotNet/Disassemblers/ClrMdDisassembler.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Code; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Extensions; @@ -96,7 +97,7 @@ internal DisassemblyResult AttachAndDisassemble(ClrMdArgs args) // we don't want to export the disassembler entry point method which is just an artificial method added to get generic types working var filteredMethods = disassembledMethods.Length == 1 ? disassembledMethods // if there is only one method we want to return it (most probably benchmark got inlined) - : disassembledMethods.Where(method => !method.Name.Contains(DisassemblerConstants.DisassemblerEntryMethodName)).ToArray(); + : disassembledMethods.Where(method => !method.Name.Contains(RunnableConstants.ForDisassemblyDiagnoserMethodName)).ToArray(); return new DisassemblyResult { diff --git a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs index c7b5ddf428..4d5ac95fea 100644 --- a/src/BenchmarkDotNet/Disassemblers/DataContracts.cs +++ b/src/BenchmarkDotNet/Disassemblers/DataContracts.cs @@ -211,11 +211,6 @@ public sealed class DisassemblyResult public Dictionary AddressToNameMapping { get; set; } = []; } -public static class DisassemblerConstants -{ - public const string DisassemblerEntryMethodName = "__ForDisassemblyDiagnoser__"; -} - internal sealed class State { internal State(ClrRuntime runtime, string targetFrameworkMoniker) diff --git a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs index 9ca2cbda84..dce120c537 100644 --- a/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs +++ b/src/BenchmarkDotNet/Disassemblers/DisassemblyDiagnoser.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Code; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Detectors; using BenchmarkDotNet.Disassemblers; @@ -79,7 +80,7 @@ private ClrMdArgs BuildClrMdArgs(BenchmarkCase benchmarkCase, string typeName, i => new( processId: processId, typeName: typeName, - methodName: DisassemblerConstants.DisassemblerEntryMethodName, + methodName: RunnableConstants.ForDisassemblyDiagnoserMethodName, printSource: Config.PrintSource, maxDepth: Config.MaxDepth, filters: Config.Filters, diff --git a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs index 0e222adb28..b9d45340a0 100644 --- a/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs +++ b/src/BenchmarkDotNet/Engines/BenchmarkSynchronizationContext.cs @@ -36,6 +36,15 @@ public void Dispose() public T ExecuteUntilComplete(ValueTask valueTask) => context.ExecuteUntilComplete(valueTask); + + public void ExecuteUntilComplete(ValueTask valueTask) + => context.ExecuteUntilComplete(WithResult(valueTask)); + + private static async ValueTask WithResult(ValueTask valueTask) + { + await valueTask.ConfigureAwait(false); + return true; + } } // We implement a specialized context that does not inherit from SynchronizationContext, because we never install a SynchronizationContext.Current. diff --git a/src/BenchmarkDotNet/Exporters/FullNameProvider.cs b/src/BenchmarkDotNet/Exporters/FullNameProvider.cs index 19555908d2..c8d32a20e0 100644 --- a/src/BenchmarkDotNet/Exporters/FullNameProvider.cs +++ b/src/BenchmarkDotNet/Exporters/FullNameProvider.cs @@ -134,8 +134,6 @@ private static string GetArgument(object? argumentValue, Type? argumentType) { case null: return "null"; - case IParam iparam: - return GetArgument(iparam.Value, argumentType); case object[] array when array.Length == 1: return GetArgument(array[0], argumentType); case string text: diff --git a/src/BenchmarkDotNet/Extensions/Polyfills/AsyncEnumerable.cs b/src/BenchmarkDotNet/Extensions/Polyfills/AsyncEnumerable.cs new file mode 100644 index 0000000000..0c946f1c0e --- /dev/null +++ b/src/BenchmarkDotNet/Extensions/Polyfills/AsyncEnumerable.cs @@ -0,0 +1,40 @@ +#if !NET10_0_OR_GREATER +namespace System.Linq; + +// System.Linq.AsyncEnumerable ships in the shared framework from .NET 10. BenchmarkDotNet provides the two members it +// uses rather than taking the package, because the rest of that surface captures the ambient SynchronizationContext, +// which BenchmarkDotNet must never do while its pump owns the calling thread. See CompositeValidator.ValidateAsync. +internal static class AsyncEnumerable +{ + public static IAsyncEnumerable Empty() + => EmptyAsyncEnumerable.Instance; + + public static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) + { + ArgumentNullException.ThrowIfNull(source); + + return FromIterator(source); + + static async IAsyncEnumerable FromIterator(IEnumerable source) + { + foreach (TSource element in source) + { + yield return element; + } + } + } + + private sealed class EmptyAsyncEnumerable : IAsyncEnumerable, IAsyncEnumerator + { + public static readonly EmptyAsyncEnumerable Instance = new(); + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => this; + + public TSource Current => default!; + + public ValueTask MoveNextAsync() => new(false); + + public ValueTask DisposeAsync() => default; + } +} +#endif diff --git a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs index 1bfaf346ce..ee6fcd2577 100644 --- a/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs +++ b/src/BenchmarkDotNet/Extensions/ReflectionExtensions.cs @@ -7,6 +7,9 @@ namespace BenchmarkDotNet.Extensions { internal static class ReflectionExtensions { + // The name the compiler gives an `implicit operator`; there is no reflection API that spells it. + internal const string OpImplicitMethodName = "op_Implicit"; + internal static T? ResolveAttribute(this Type? type) where T : Attribute => type?.GetTypeInfo().GetCustomAttributes(typeof(T), false).OfType().FirstOrDefault(); @@ -16,21 +19,16 @@ internal static class ReflectionExtensions internal static bool HasAttribute(this MemberInfo? memberInfo) where T : Attribute => memberInfo.ResolveAttribute() != null; - internal static bool IsNullable(this Type type) => Nullable.GetUnderlyingType(type) != null; - - public static bool IsInitOnly(this PropertyInfo propertyInfo) - { - var setMethodReturnParameter = propertyInfo.SetMethod?.ReturnParameter; - if (setMethodReturnParameter == null) - return false; - - var isExternalInitType = typeof(System.Runtime.CompilerServices.Unsafe).Assembly - .GetType("System.Runtime.CompilerServices.IsExternalInit"); - if (isExternalInitType == null) - return false; + /// + /// The value to pass for an omitted optional argument. A parameter can be optional without declaring a + /// default ([Optional] with no [DefaultParameterValue]); null is right for those, because Invoke converts it + /// to default(T) even for a value type. Type.Missing is not: Invoke(object, object[]) does no + /// optional-parameter binding and rejects it. + /// + internal static object? GetDefaultArgumentValue(this ParameterInfo parameter) + => parameter.HasDefaultValue ? parameter.DefaultValue : null; - return setMethodReturnParameter.GetRequiredCustomModifiers().Contains(isExternalInitType); - } + internal static bool IsNullable(this Type type) => Nullable.GetUnderlyingType(type) != null; /// /// returns type name which can be used in generated C# code @@ -210,13 +208,14 @@ private static MethodInfo[] GetBenchmarks(this TypeInfo typeInfo) .Where(method => method.GetCustomAttributes(true).OfType().Any()) .ToArray(); - internal static (string Name, TAttribute Attribute, bool IsStatic, Type ParameterType)[] + internal static (string Name, TAttribute Attribute, bool IsStatic, Type ParameterType, MemberInfo Member)[] GetTypeMembersWithGivenAttribute(this Type type, BindingFlags reflectionFlags) where TAttribute : Attribute { var fields = type .GetFields(reflectionFlags) .Select(f => Create( + f, f.Name, f.ResolveAttribute(), f.IsStatic, @@ -225,25 +224,51 @@ internal static (string Name, TAttribute Attribute, bool IsStatic, Type Paramete var properties = type .GetProperties(reflectionFlags) .Select(p => Create( + p, p.Name, p.ResolveAttribute(), p.GetSetMethod()?.IsStatic == true, p.PropertyType)); - return fields.Concat(properties) - .WhereNotNull() - .Select(x => x!.Value) + // One entry per name, keeping the most derived declaration. GetFields hands back a hidden base field + // alongside the `new` one hiding it - GetProperties collapses the pair, so only fields arrive twice - + // and the name binds to the most derived declaration everywhere it is then used. A second entry + // becomes a second parameter of the same name: it multiplies the cases against itself and emits the + // name twice in the runnable's object initializer, which is CS1912. + var found = new List<(MemberInfo Member, string Name, TAttribute Attribute, bool IsStatic, Type MemberType)>(); + var indexByName = new Dictionary(StringComparer.Ordinal); + + foreach (var candidate in fields.Concat(properties).WhereNotNull().Select(x => x!.Value)) + { + if (!indexByName.TryGetValue(candidate.Name, out int index)) + { + indexByName.Add(candidate.Name, found.Count); + found.Add(candidate); + } + else if (found[index].Member.DeclaringType!.IsAssignableFrom(candidate.Member.DeclaringType)) + { + found[index] = candidate; + } + } + + return found + .Select(x => (x.Name, x.Attribute, x.IsStatic, x.MemberType, x.Member)) .ToArray(); - static (string Name, TAttribute Attribute, bool IsStatic, Type MemberType)? - Create(string name, TAttribute? attribute, bool isStatic, Type memberType) + static (MemberInfo Member, string Name, TAttribute Attribute, bool IsStatic, Type MemberType)? + Create(MemberInfo member, string name, TAttribute? attribute, bool isStatic, Type memberType) { if (attribute == null) return null; - return (name, attribute, isStatic, memberType); + return (member, name, attribute, isStatic, memberType); } } + // What a parameter takes, ref/in/out set aside: reflection reports those as byref types - `ref T` is `T&`, + // which nothing is castable to - though the modifier says how the argument travels, not what it is. + internal static Type WithoutRefModifier(this Type parameterType) + => parameterType.IsByRef ? parameterType.GetElementType()! : parameterType; + internal static bool IsStackOnlyWithImplicitCast(this Type argumentType, [NotNullWhen(true)] object? argumentInstance) { if (argumentInstance == null) @@ -254,17 +279,26 @@ internal static bool IsStackOnlyWithImplicitCast(this Type argumentType, [NotNul var instanceType = argumentInstance.GetType(); - var implicitCastsDefinedInArgumentInstance = instanceType.GetMethods().Where(method => method.Name == "op_Implicit" && method.GetParameters().Any()).ToArray(); - if (implicitCastsDefinedInArgumentInstance.Any(implicitCast => implicitCast.ReturnType == argumentType && implicitCast.GetParameters().All(p => p.ParameterType == instanceType))) - return true; - - var implicitCastsDefinedInArgumentType = argumentType.GetMethods().Where(method => method.Name == "op_Implicit" && method.GetParameters().Any()).ToArray(); - if (implicitCastsDefinedInArgumentType.Any(implicitCast => implicitCast.ReturnType == argumentType && implicitCast.GetParameters().All(p => p.ParameterType == instanceType))) - return true; - - return false; + return HasImplicitConversion(argumentType, instanceType); } + private static bool HasImplicitConversion(Type targetType, Type sourceType) + => DeclaresConversion(sourceType, targetType, sourceType) + || DeclaresConversion(targetType, targetType, sourceType); + + // An `implicit operator` written for exactly these types. Only exactly: a source is admitted by naming + // what the parameter takes, so there is no conversion to reason about on the way into the operator - and + // reasoning about one is what reflection cannot do, since it answers the CLR's rules rather than C#'s. + // + // C# gathers operators from both types and their base classes. Reflection withholds a base's statics + // without FlattenHierarchy, so without it an operator inherited from a base is invisible. + private static bool DeclaresConversion(Type declaringType, Type targetType, Type sourceType) + => declaringType.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Any(method => method.Name == OpImplicitMethodName + && method.ReturnType == targetType + && method.GetParameters() is { Length: 1 } parameters + && parameters[0].ParameterType == sourceType); + private static bool IsRunnableGenericType(TypeInfo typeInfo) => // if it is an open generic - there must be GenericBenchmark attributes (!typeInfo.IsGenericTypeDefinition || typeInfo.GenericTypeArguments.Any() || typeInfo.GetCustomAttributes(true).OfType().Any()) @@ -309,17 +343,12 @@ internal static bool IsAwaitable(this Type type, [NotNullWhen(true)] out Awaitab internal static bool IsAsyncEnumerable(this Type type, [NotNullWhen(true)] out AsyncEnumerableInfo? info) { - // 1. Pattern first: a public instance GetAsyncEnumerator with all-optional parameters whose - // return type has a public instance MoveNextAsync awaitable-to-bool (also accepting - // all-optional params) and a public instance Current property. Roslyn's `await foreach` - // binds to this in preference to the interface, so we mirror that order. The element type - // comes from Current so it tracks what the compiler binds to, even if the type also - // implements IAsyncEnumerable for a different U. (Extension GetAsyncEnumerator is not - // handled.) - // - // Note: when the type IS exactly IAsyncEnumerable, `GetMethods(Public|Instance)` returns - // the interface's own GetAsyncEnumerator, so this branch also handles that case naturally — - // we just flag it as interface dispatch via the conditional below. + // 1. Pattern first, as `await foreach` binds: a public instance GetAsyncEnumerator with all-optional parameters, + // returning a type with a public MoveNextAsync awaitable-to-bool (also accepting all-optional params) and a + // public Current property. The element type comes from Current, so it tracks what the compiler binds to even + // when the type also implements IAsyncEnumerable for another U. Extension GetAsyncEnumerator is not handled. + // IAsyncEnumerable itself lands here too - GetMethods returns the interface's own - and the conditional + // below flags it as interface dispatch. var patternGetAsyncEnumerator = type .GetMethods(BindingFlags.Public | BindingFlags.Instance) .FirstOrDefault(m => m.Name == nameof(IAsyncEnumerable<>.GetAsyncEnumerator) @@ -357,21 +386,7 @@ internal static bool IsAsyncEnumerable(this Type type, [NotNullWhen(true)] out A { if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) { - var ifaceItemType = iface.GetGenericArguments()[0]; - var ifaceEnumeratorType = typeof(IAsyncEnumerator<>).MakeGenericType(ifaceItemType); - var ifaceMoveNextAsync = ifaceEnumeratorType.GetMethod(nameof(IAsyncEnumerator<>.MoveNextAsync))!; - // `MoveNextAsync` on `IAsyncEnumerator` returns `ValueTask` which always - // satisfies the awaitable shape; pull the resolved `AwaitableInfo` from IsAwaitable - // rather than constructing it by hand. - ifaceMoveNextAsync.ReturnType.IsAwaitable(out var ifaceMoveNextAwaitable); - info = new AsyncEnumerableInfo( - ifaceItemType, - ifaceEnumeratorType, - iface.GetMethod(nameof(IAsyncEnumerable<>.GetAsyncEnumerator))!, - ifaceMoveNextAsync, - ifaceMoveNextAwaitable!, - ifaceEnumeratorType.GetProperty(nameof(IAsyncEnumerator<>.Current))!, - IsInterfaceDispatch: true); + info = GetAsyncEnumerableInterfaceInfo(iface.GetGenericArguments()[0]); return true; } } @@ -379,6 +394,170 @@ internal static bool IsAsyncEnumerable(this Type type, [NotNullWhen(true)] out A return false; } + // The await-foreach members of IAsyncEnumerable itself, for a caller that has already established the + // interface and wants it bound in preference to any pattern method the concrete type may also declare. + // Every member is the interface's own, so invoking them dispatches to the implementation whether it is + // implicit or explicit. + internal static AsyncEnumerableInfo GetAsyncEnumerableInterfaceInfo(Type elementType) + { + var interfaceType = typeof(IAsyncEnumerable<>).MakeGenericType(elementType); + var enumeratorType = typeof(IAsyncEnumerator<>).MakeGenericType(elementType); + var moveNextAsync = enumeratorType.GetMethod(nameof(IAsyncEnumerator<>.MoveNextAsync))!; + // `MoveNextAsync` on `IAsyncEnumerator` returns `ValueTask` which always satisfies the + // awaitable shape; pull the resolved `AwaitableInfo` from IsAwaitable rather than building it by hand. + moveNextAsync.ReturnType.IsAwaitable(out var moveNextAwaitable); + return new AsyncEnumerableInfo( + elementType, + enumeratorType, + interfaceType.GetMethod(nameof(IAsyncEnumerable<>.GetAsyncEnumerator))!, + moveNextAsync, + moveNextAwaitable!, + enumeratorType.GetProperty(nameof(IAsyncEnumerator<>.Current))!, + IsInterfaceDispatch: true); + } + + // Whether the type is, or implements, IAsyncEnumerable. [ParamsSource]/[ArgumentsSource] async sources + // must use the interface (not just the await-foreach pattern), so callers reject pattern-only types. + internal static bool IsIAsyncEnumerable(this Type type, [NotNullWhen(true)] out Type? elementType) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + { + elementType = type.GetGenericArguments()[0]; + return true; + } + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) + { + elementType = iface.GetGenericArguments()[0]; + return true; + } + } + elementType = null; + return false; + } + + // Mirrors AsyncTypeShapes.CountSourceShapes on the analyzer side. The generated extraction call infers its + // element type from the source, and type inference needs a *unique* candidate interface, so a source with + // anything other than exactly one instantiation across the two shapes fails to compile (CS0411) - even when + // one element type converts to the other, as with IEnumerable plus IEnumerable. + internal static int CountSourceShapes(this Type type) + => type.CountInstantiations(typeof(IEnumerable<>)) + type.CountInstantiations(typeof(IAsyncEnumerable<>)); + + private static int CountInstantiations(this Type type, Type interfaceDefinition) + { + var found = new HashSet(); + if (type.IsGenericType && type.GetGenericTypeDefinition() == interfaceDefinition) + found.Add(type); + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType && iface.GetGenericTypeDefinition() == interfaceDefinition) + found.Add(iface); + } + return found.Count; + } + + // The element type a source declares, which is the type the generated extraction call returns and therefore + // the type the generated code indexes into. Only an unambiguous shape has one - see CountSourceShapes. + internal static bool TryGetSourceElementType(this Type sourceReturnType, [NotNullWhen(true)] out Type? elementType) + { + if (sourceReturnType.CountSourceShapes() == 1) + { + foreach (var candidate in sourceReturnType.GetInterfaces().Prepend(sourceReturnType)) + { + if (candidate.IsGenericType + && (candidate.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || candidate.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))) + { + elementType = candidate.GetGenericArguments()[0]; + return true; + } + } + } + elementType = null; + return false; + } + + // The member a [ParamsSource]/[ArgumentsSource] name resolves to: a public method whose parameters are all + // optional, else a property with a public getter. The single resolver - discovery reads its values from + // whatever this returns and SourceReturnTypeValidator reports on the same member, so the two cannot judge + // different members of the same name. A generic method definition is passed over rather than matched, so a + // property of that name serves the name instead of an unusable method. + internal static MemberInfo? FindSourceMember(this Type sourceType, string sourceName) + => (MemberInfo?) sourceType.GetAllMethods() + .FirstOrDefault(method => method.Name == sourceName && method.IsPublic + && !method.IsGenericMethodDefinition + && method.GetParameters().All(parameter => parameter.IsOptional)) + ?? sourceType.GetAllProperties() + .FirstOrDefault(property => property.Name == sourceName && property.GetMethod?.IsPublic == true); + + /// + /// The members a benchmark's parameters are looked for on. FlattenHierarchy is what reaches a base type's + /// statics - without it reflection returns inherited *instance* members only. Discovery and every validator + /// reporting on parameter members share this, so none can judge a member another cannot see. Not to be + /// confused with , which is its opposite. + /// + internal const BindingFlags ParameterMemberFlags = + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.FlattenHierarchy; + + /// + /// The property or field a parameter is written through, matched on the declared type discovery recorded as + /// well as the name. These flags reach a hidden base member beside the one hiding it, and reflection returns + /// members in no particular order, so the most derived declaration is chosen here. The choice spans both + /// kinds: a field can hide a property, and looking for one kind first would find the base member of that + /// kind and never reach the derived member that is the parameter. + /// + internal static MemberInfo? GetParameterMember(this Type type, string name, Type parameterType, BindingFlags flags) + { + MemberInfo? found = null; + foreach (var member in type.GetMembers(flags)) + { + var memberType = member switch + { + // An indexer takes arguments and is never a parameter member. + PropertyInfo property when property.GetIndexParameters().Length == 0 => property.PropertyType, + FieldInfo field => field.FieldType, + _ => null + }; + + if (memberType != parameterType || member.Name != name) + continue; + if (found is null || found.DeclaringType!.IsAssignableFrom(member.DeclaringType)) + found = member; + } + return found; + } + + // DeclaredOnly because the member sought is declared on the type being searched, and a metadata token is + // unique only within a module. Inherited members are what ParameterMemberFlags exists to reach. + private const BindingFlags DeclaredMemberFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly; + + // The member as `contextType`'s generic definition names it - on that definition, or on the base in its + // hierarchy that declares it. The base is read as the derived type writes it, `Base` carrying the derived + // type's own T, so inherited and locally declared members share type parameters. Null when it is elsewhere. + internal static MemberInfo? GetDeclaredMemberIn(this MemberInfo member, Type contextType) + { + if (member.DeclaringType is not { } declaringType) + return null; + + var declaringDefinition = declaringType.IsGenericType ? declaringType.GetGenericTypeDefinition() : declaringType; + + for (var candidate = contextType.IsGenericType ? contextType.GetGenericTypeDefinition() : contextType; candidate != null; candidate = candidate.BaseType) + { + if ((candidate.IsGenericType ? candidate.GetGenericTypeDefinition() : candidate) != declaringDefinition) + continue; + + return candidate.GetMembers(DeclaredMemberFlags) + .FirstOrDefault(inherited => inherited.MetadataToken == member.MetadataToken); + } + + return null; + } + + // The type a source member hands back, which is what the generated code infers the element type from. + internal static Type GetSourceReturnType(this MemberInfo source) + => source is PropertyInfo property ? property.GetMethod!.ReturnType : ((MethodInfo) source).ReturnType; + internal static Attribute? GetAsyncMethodBuilderAttribute(this MemberInfo memberInfo) // AsyncMethodBuilderAttribute can come from any assembly, so we need to use reflection by name instead of searching for the exact type. => memberInfo.GetCustomAttributes(false).FirstOrDefault(attr => attr.GetType().FullName == typeof(AsyncMethodBuilderAttribute).FullName) as Attribute; diff --git a/src/BenchmarkDotNet/Helpers/DisposeHelper.cs b/src/BenchmarkDotNet/Helpers/DisposeHelper.cs new file mode 100644 index 0000000000..fb1ac81a8b --- /dev/null +++ b/src/BenchmarkDotNet/Helpers/DisposeHelper.cs @@ -0,0 +1,31 @@ +using System.Runtime.ExceptionServices; + +namespace BenchmarkDotNet.Helpers; + +internal static class DisposeHelper +{ + public static async ValueTask DisposeAllAsync(this IEnumerable asyncDisposables) + { + List? exceptions = null; + foreach (var asyncDisposable in asyncDisposables) + { + try + { + await asyncDisposable.DisposeAsync().ConfigureAwait(); + } + catch (Exception ex) + { + exceptions ??= []; + exceptions.Add(ex); + } + } + switch (exceptions) + { + case [var exception]: + ExceptionDispatchInfo.Capture(exception).Throw(); + break; // unreachable + case not null: + throw new AggregateException(exceptions); + } + } +} diff --git a/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs b/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs index 0523f3ab6e..a312c89e98 100644 --- a/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs +++ b/src/BenchmarkDotNet/Helpers/DynamicAwaitHelper.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Extensions; using System.Reflection; +using System.Runtime.ExceptionServices; using System.Runtime.CompilerServices; namespace BenchmarkDotNet.Helpers; @@ -12,32 +13,45 @@ internal static class DynamicAwaitHelper return (awaitableInfo.ResultType != typeof(void), result); } - internal static ValueTask DrainAsyncEnumerableAsync(object asyncEnumerable, AsyncEnumerableInfo enumerableInfo) - => EnumerateCoreAsync(asyncEnumerable, enumerableInfo, items: null); + internal static IAsyncEnumerable EnumerateSourceAsync(object asyncEnumerable, Type elementType) + // Sources are always read through IAsyncEnumerable, pattern-based await foreach types are not supported. + // A reference-type element needs no reflection at all: IAsyncEnumerable is covariant. + => !elementType.IsValueType + ? (IAsyncEnumerable) asyncEnumerable + : EnumerateSourceAsyncCore(asyncEnumerable, ReflectionExtensions.GetAsyncEnumerableInterfaceInfo(elementType)); - internal static async ValueTask> ToListAsync(object asyncEnumerable, AsyncEnumerableInfo enumerableInfo) + private static async IAsyncEnumerable EnumerateSourceAsyncCore(object asyncEnumerable, AsyncEnumerableInfo asyncEnumerableInfo, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - List items = []; - await EnumerateCoreAsync(asyncEnumerable, enumerableInfo, items).ConfigureAwait(false); - return items; + var enumerator = Unwrapped(() => asyncEnumerableInfo.GetAsyncEnumeratorMethod.Invoke(asyncEnumerable, [cancellationToken]))!; + var moveNextAsyncArgs = GetDefaultArgs(asyncEnumerableInfo.MoveNextAsyncMethod); + + try + { + while (await ((ValueTask) Unwrapped(() => asyncEnumerableInfo.MoveNextAsyncMethod.Invoke(enumerator, moveNextAsyncArgs))!).ConfigureAwait(false)) + { + yield return Unwrapped(() => asyncEnumerableInfo.CurrentProperty.GetValue(enumerator)); + } + } + finally + { + await ((IAsyncDisposable) enumerator).DisposeAsync().ConfigureAwait(false); + } } - private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncEnumerableInfo enumerableInfo, List? items) + internal static async IAsyncEnumerable EnumerateBenchmarkAsync(object asyncEnumerable, AsyncEnumerableInfo asyncEnumerableInfo) { - var getAsyncEnumeratorArgs = GetDefaultArgs(enumerableInfo.GetAsyncEnumeratorMethod); - var enumerator = enumerableInfo.GetAsyncEnumeratorMethod.Invoke(asyncEnumerable, getAsyncEnumeratorArgs)!; + var enumerator = Unwrapped(() => asyncEnumerableInfo.GetAsyncEnumeratorMethod.Invoke(asyncEnumerable, GetDefaultArgs(asyncEnumerableInfo.GetAsyncEnumeratorMethod)))!; - var moveNextAsyncArgs = GetDefaultArgs(enumerableInfo.MoveNextAsyncMethod); - var currentProperty = enumerableInfo.CurrentProperty; - var moveNextAwaitable = enumerableInfo.MoveNextAwaitable; + var moveNextAsyncArgs = GetDefaultArgs(asyncEnumerableInfo.MoveNextAsyncMethod); + var currentProperty = asyncEnumerableInfo.CurrentProperty; + var moveNextAwaitable = asyncEnumerableInfo.MoveNextAwaitable; - // DisposeAsync is optional for the await-foreach pattern. Roslyn matches a public instance - // method named DisposeAsync whose parameters are all optional and whose return type satisfies - // the awaitable pattern with a void GetResult; otherwise it falls back to the IAsyncDisposable - // interface dispatch. + // DisposeAsync is optional in the await-foreach pattern: Roslyn matches a public instance DisposeAsync + // whose parameters are all optional and whose return type is awaitable with a void GetResult, else the + // IAsyncDisposable interface. MethodInfo? disposeAsyncMethod = null; AwaitableInfo? disposeAwaitableInfo = null; - foreach (var candidate in enumerableInfo.EnumeratorType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) + foreach (var candidate in asyncEnumerableInfo.EnumeratorType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if (candidate.Name == nameof(IAsyncDisposable.DisposeAsync) && candidate.GetParameters().All(p => p.IsOptional) @@ -49,7 +63,7 @@ private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncE break; } } - if (disposeAsyncMethod is null && typeof(IAsyncDisposable).IsAssignableFrom(enumerableInfo.EnumeratorType)) + if (disposeAsyncMethod is null && typeof(IAsyncDisposable).IsAssignableFrom(asyncEnumerableInfo.EnumeratorType)) { disposeAsyncMethod = typeof(IAsyncDisposable).GetMethod(nameof(IAsyncDisposable.DisposeAsync))!; disposeAsyncMethod.ReturnType.IsAwaitable(out disposeAwaitableInfo); @@ -60,20 +74,20 @@ private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncE { while (true) { - var moveNextResult = enumerableInfo.MoveNextAsyncMethod.Invoke(enumerator, moveNextAsyncArgs); + var moveNextResult = Unwrapped(() => asyncEnumerableInfo.MoveNextAsyncMethod.Invoke(enumerator, moveNextAsyncArgs)); bool hasMore = (bool)(await new DynamicAwaitable(moveNextAwaitable, moveNextResult!))!; if (!hasMore) { break; } - items?.Add(currentProperty.GetValue(enumerator)); + yield return Unwrapped(() => currentProperty.GetValue(enumerator)); } } finally { if (disposeAsyncMethod != null) { - var disposeResult = disposeAsyncMethod.Invoke(enumerator, disposeAsyncArgs); + var disposeResult = Unwrapped(() => disposeAsyncMethod.Invoke(enumerator, disposeAsyncArgs)); if (disposeResult != null) { await new DynamicAwaitable(disposeAwaitableInfo!, disposeResult); @@ -92,7 +106,7 @@ private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncE var args = new object?[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { - args[i] = parameters[i].HasDefaultValue ? parameters[i].DefaultValue : null; + args[i] = parameters[i].GetDefaultArgumentValue(); } return args; } @@ -100,16 +114,51 @@ private static async ValueTask EnumerateCoreAsync(object asyncEnumerable, AsyncE private readonly struct DynamicAwaitable(AwaitableInfo awaitableInfo, object awaitable) { public DynamicAwaiter GetAwaiter() - => new(awaitableInfo, awaitableInfo.GetAwaiterMethod.Invoke(awaitable, null)); + { + // Read into locals: a lambda in a struct's instance member cannot capture a primary constructor parameter. + var info = awaitableInfo; + object target = awaitable; + + return new(info, Unwrapped(() => info.GetAwaiterMethod.Invoke(target, null))); + } + } + + // Reflection wraps exceptions in TargetInvocationException, while the covariance path lets it through as thrown. + // Here we unwrap it and rethrow while preserving its stacktrace so both paths exceptions behave consistently. + private static object? Unwrapped(Func invoke) + { + try + { + return invoke(); + } + catch (TargetInvocationException exception) when (exception.InnerException is { } inner) + { + ExceptionDispatchInfo.Capture(inner).Throw(); + throw; // Not reached: Throw() always throws. + } } private readonly struct DynamicAwaiter(AwaitableInfo awaitableInfo, object? awaiter) : ICriticalNotifyCompletion { public bool IsCompleted - => awaitableInfo.IsCompletedProperty.GetMethod!.Invoke(awaiter, null) is true; + { + get + { + var isCompleted = awaitableInfo.IsCompletedProperty.GetMethod!; + object? target = awaiter; + + return Unwrapped(() => isCompleted.Invoke(target, null)) is true; + } + } public object? GetResult() - => awaitableInfo.GetResultMethod.Invoke(awaiter, null); + { + // Read into locals: a lambda in a struct's instance member cannot capture a primary constructor parameter. + var getResult = awaitableInfo.GetResultMethod; + object? target = awaiter; + + return Unwrapped(() => getResult.Invoke(target, null)); + } public void OnCompleted(Action continuation) => OnCompletedCore(typeof(INotifyCompletion), nameof(INotifyCompletion.OnCompleted), continuation); @@ -119,14 +168,41 @@ public void UnsafeOnCompleted(Action continuation) private void OnCompletedCore(Type interfaceType, string methodName, Action continuation) { - var onCompletedMethod = interfaceType.GetMethod(methodName); + // ICriticalNotifyCompletion is optional in the awaiter pattern, but DynamicAwaiter declares it, so a + // state machine awaiting one always takes UnsafeOnCompleted. Asking GetInterfaceMap for an interface the + // user's awaiter does not implement throws, so hand those to OnCompleted - which flows the execution + // context that UnsafeOnCompleted exists to skip, the safe direction. + if (interfaceType == typeof(ICriticalNotifyCompletion) + && !typeof(ICriticalNotifyCompletion).IsAssignableFrom(awaitableInfo.AwaiterType)) + { + OnCompletedCore(typeof(INotifyCompletion), nameof(INotifyCompletion.OnCompleted), continuation); + return; + } + + var onCompletedMethod = interfaceType.GetMethod(methodName)!; + + // The awaiter pattern binds on the awaiter's declared type, which may itself be an interface - + // GetInterfaceMap refuses one ("'this' type cannot be an interface itself"), and the throw lands on + // AwaitUnsafeOnCompleted, where it is rethrown onto the thread pool and takes the process down. No map + // is needed here anyway: invoking the interface method dispatches to whatever implements it, an + // explicit implementation included. + if (awaitableInfo.AwaiterType.IsInterface) + { + object? interfaceTarget = awaiter; + Unwrapped(() => onCompletedMethod.Invoke(interfaceTarget, [continuation])); + return; + } + var map = awaitableInfo.AwaiterType.GetInterfaceMap(interfaceType); for (int i = 0; i < map.InterfaceMethods.Length; i++) { if (map.InterfaceMethods[i] == onCompletedMethod) { - map.TargetMethods[i].Invoke(awaiter, [continuation]); + var onCompleted = map.TargetMethods[i]; + object? target = awaiter; + + Unwrapped(() => onCompleted.Invoke(target, [continuation])); return; } } diff --git a/src/BenchmarkDotNet/Helpers/SourceCodeHelper.cs b/src/BenchmarkDotNet/Helpers/SourceCodeHelper.cs index ea2916e00a..5d2f4571a2 100644 --- a/src/BenchmarkDotNet/Helpers/SourceCodeHelper.cs +++ b/src/BenchmarkDotNet/Helpers/SourceCodeHelper.cs @@ -8,6 +8,16 @@ namespace BenchmarkDotNet.Helpers { public static class SourceCodeHelper { + /// + /// Renders a value as a C# expression, using to disambiguate the value. + /// An enum declared in F# is erased to its underlying type in attribute metadata, so the value alone + /// would render as a plain number (dotnet/fsharp#995). + /// + public static string ToSourceCode(object? value, Type declaredType) + => value != null && declaredType.IsEnum && !value.GetType().IsEnum + ? ToSourceCode(Enum.ToObject(declaredType, value)) + : ToSourceCode(value); + public static string ToSourceCode(object? value) { switch (value) @@ -65,7 +75,7 @@ public static string ToSourceCode(object? value) return value.ToString()!; } - public static bool IsCompilationTimeConstant(object value) + public static bool IsCompilationTimeConstant(object? value) => value == null || IsCompilationTimeConstant(value.GetType()); public static bool IsCompilationTimeConstant(Type type) diff --git a/src/BenchmarkDotNet/Parameters/ArrayDisplay.cs b/src/BenchmarkDotNet/Parameters/ArrayDisplay.cs new file mode 100644 index 0000000000..cb5d10f99a --- /dev/null +++ b/src/BenchmarkDotNet/Parameters/ArrayDisplay.cs @@ -0,0 +1,32 @@ +using BenchmarkDotNet.Extensions; + +namespace BenchmarkDotNet.Parameters; + +// Renders an array parameter's value for the Params column, e.g. "Int32[3]" or "Int32[2,2][]". +internal static class ArrayDisplay +{ + public static string GetDisplayString(Array array) + { + string dimensionRepr = string.Join(", ", Enumerable.Range(0, array.Rank).Select(array.GetLength)); + + var (baseElementTypeRepr, innerDimensions) = GetDisplayString(array.GetType()); + + innerDimensions = string.Join("", innerDimensions.Split([']'], count: 2).Skip(1)); + + return $"{baseElementTypeRepr}[{dimensionRepr}]{innerDimensions}"; + } + + private static (string BaseElementTypeRepr, string InnerDimensions) GetDisplayString(Type arrayType) + { + var elemType = arrayType.GetElementType()!; + + if (elemType.IsArray) + { + var (baseElementTypeRepr, innerDimensions) = GetDisplayString(elemType); + + return (baseElementTypeRepr, $"[{new string(',', arrayType.GetArrayRank() - 1)}]{innerDimensions}"); + } + + return (elemType.GetDisplayName(), $"[{new string(',', arrayType.GetArrayRank() - 1)}]"); + } +} diff --git a/src/BenchmarkDotNet/Parameters/ParameterDefinition.cs b/src/BenchmarkDotNet/Parameters/ParameterDefinition.cs index 81295d964b..e450ca6218 100644 --- a/src/BenchmarkDotNet/Parameters/ParameterDefinition.cs +++ b/src/BenchmarkDotNet/Parameters/ParameterDefinition.cs @@ -1,22 +1,21 @@ -namespace BenchmarkDotNet.Parameters +namespace BenchmarkDotNet.Parameters { - public class ParameterDefinition + /// + /// What a benchmark parameter is, independently of any value it takes: a member the runnable assigns, or a + /// parameter the benchmark method is called with. + /// + public sealed class ParameterDefinition(string name, bool isStatic, bool isArgument, Type parameterType, int priorityInCategory) { - public string Name { get; } - public bool IsStatic { get; } - public object?[] Values { get; } - public bool IsArgument { get; } - public Type ParameterType { get; } - public int PriorityInCategory { get; } + public string Name { get; } = name; - public ParameterDefinition(string name, bool isStatic, object?[] values, bool isArgument, Type parameterType, int priorityInCategory) - { - Name = name; - IsStatic = isStatic; - Values = values; - IsArgument = isArgument; - ParameterType = parameterType; - PriorityInCategory = priorityInCategory; - } + /// Whether the member holding the value is static. Always false for an argument, which is not a member. + public bool IsStatic { get; } = isStatic; + + /// Whether the benchmark method is called with this parameter, rather than assigned it. + public bool IsArgument { get; } = isArgument; + + public Type ParameterType { get; } = parameterType; + + public int PriorityInCategory { get; } = priorityInCategory; } -} \ No newline at end of file +} diff --git a/src/BenchmarkDotNet/Parameters/ParameterDefinitions.cs b/src/BenchmarkDotNet/Parameters/ParameterDefinitions.cs deleted file mode 100644 index 1f8a1e54ff..0000000000 --- a/src/BenchmarkDotNet/Parameters/ParameterDefinitions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using BenchmarkDotNet.Extensions; -using BenchmarkDotNet.Reports; -using JetBrains.Annotations; - -namespace BenchmarkDotNet.Parameters -{ - public class ParameterDefinitions - { - [PublicAPI] public IReadOnlyList Items { get; } - - public ParameterDefinitions(IReadOnlyList items) => Items = items; - - public IReadOnlyList Expand(SummaryStyle summaryStyle) => Expand([new ParameterInstances(new List())], Items, summaryStyle); - - private static IReadOnlyList Expand(IReadOnlyList instancesList, IReadOnlyList definitions, SummaryStyle summaryStyle) - { - if (definitions.IsNullOrEmpty()) - return instancesList; - var nextDefinition = definitions.First(); - var newInstancesList = new List(); - foreach (var instances in instancesList) - { - foreach (var value in nextDefinition.Values) - { - var items = new List(); - items.AddRange(instances.Items); - items.Add(new ParameterInstance(nextDefinition, value, summaryStyle)); - newInstancesList.Add(new ParameterInstances(items)); - } - } - return Expand(newInstancesList, definitions.Skip(1).ToArray(), summaryStyle); - } - - public override string ToString() => Items.Any() ? string.Join(",", Items.Select(item => item.Name)) : ""; - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Parameters/ParameterInstance.cs b/src/BenchmarkDotNet/Parameters/ParameterInstance.cs index 41f7b88555..36447d3836 100644 --- a/src/BenchmarkDotNet/Parameters/ParameterInstance.cs +++ b/src/BenchmarkDotNet/Parameters/ParameterInstance.cs @@ -1,56 +1,98 @@ -using BenchmarkDotNet.Code; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; -using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Reports; using JetBrains.Annotations; using System.Globalization; namespace BenchmarkDotNet.Parameters { - public class ParameterInstance : IDisposable + public class ParameterInstance : IDisposable, IAsyncDisposable { public const string NullParameterTextRepresentation = "?"; [PublicAPI] public ParameterDefinition Definition { get; } - private readonly object? value; + /// + /// The value bound to this benchmark case, and the description a toolchain uses to re-create it in + /// generated code. The toolchain owns the generated syntax; this describes the value only. + /// + public ParameterValue ParameterValue { get; } + private readonly int maxParameterColumnWidthFromConfig; - public ParameterInstance(ParameterDefinition definition, object? value, SummaryStyle? summaryStyle) + public ParameterInstance(ParameterDefinition definition, ParameterValue parameterValue, SummaryStyle? summaryStyle) { Definition = definition; - this.value = value; + ParameterValue = parameterValue; maxParameterColumnWidthFromConfig = summaryStyle?.MaxParameterColumnWidth ?? SummaryStyle.DefaultMaxParameterColumnWidth; } - public void Dispose() => (Value as IDisposable)?.Dispose(); + /// Convenience overload for a value the toolchain can embed directly. + internal ParameterInstance(ParameterDefinition definition, object? value, SummaryStyle? summaryStyle) + : this(definition, new ParameterValue.Constant(value, definition.ParameterType), summaryStyle) + { + } + + public async ValueTask DisposeAsync() + { + switch (Value) + { + case IAsyncDisposable asyncDisposable: + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + break; + + case IDisposable disposable: + disposable.Dispose(); + break; + } + } + + public void Dispose() + { + switch (Value) + { + // Intentionally flipped the order from DisposeAsync to avoid sync-over-async if the value already supports sync dispose. + case IDisposable disposable: + disposable.Dispose(); + break; + case IAsyncDisposable asyncDisposable: + { + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + context.ExecuteUntilComplete(asyncDisposable.DisposeAsync()); + break; + } + } + } public string Name => Definition.Name; public bool IsStatic => Definition.IsStatic; public bool IsArgument => Definition.IsArgument; - public object? Value => value is IParam parameter ? parameter.Value : value; - - public string ToSourceCode() - => value is IParam parameter - ? parameter.ToSourceCode() - : SourceCodeHelper.ToSourceCode(value); + public object? Value => ParameterValue.Value; private string ToDisplayText(CultureInfo cultureInfo, int maxParameterColumnWidth) { - switch (value) + switch (Value) { case null: return NullParameterTextRepresentation; - case IParam parameter: - return Trim(parameter.DisplayText, maxParameterColumnWidth).EscapeSpecialCharacters(false); + case Array array: + return Trim(ArrayDisplay.GetDisplayString(array), maxParameterColumnWidth).EscapeSpecialCharacters(false); + // An enum declared in F# is erased to its underlying type in attribute metadata, so the declared + // type is what names the member (dotnet/fsharp#995). Matched on that underlying type exactly, + // because a source can yield anything: any other value is a mismatch for someone else to report, + // not something to crash Enum.ToObject on while rendering a display name. + case var _ when Definition.ParameterType is { IsEnum: true } + && !Value!.GetType().IsEnum + && Value.GetType() == Enum.GetUnderlyingType(Definition.ParameterType): + return Trim(Enum.ToObject(Definition.ParameterType, Value).ToString()!, maxParameterColumnWidth).EscapeSpecialCharacters(false); case IFormattable formattable: return Trim(formattable.ToString(null, cultureInfo), maxParameterColumnWidth).EscapeSpecialCharacters(false); // no trimming for types! case Type type: return type.IsNullable() ? $"{Nullable.GetUnderlyingType(type)!.GetDisplayName()}?" : type.GetDisplayName(); default: - return Trim(value.ToString()!, maxParameterColumnWidth).EscapeSpecialCharacters(false); + return Trim(Value.ToString()!, maxParameterColumnWidth).EscapeSpecialCharacters(false); } } diff --git a/src/BenchmarkDotNet/Parameters/ParameterInstances.cs b/src/BenchmarkDotNet/Parameters/ParameterInstances.cs index 39d30c5c7a..2f679026d7 100644 --- a/src/BenchmarkDotNet/Parameters/ParameterInstances.cs +++ b/src/BenchmarkDotNet/Parameters/ParameterInstances.cs @@ -1,8 +1,10 @@ +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; namespace BenchmarkDotNet.Parameters { - public class ParameterInstances : IEquatable, IDisposable + public class ParameterInstances : IDisposable, IAsyncDisposable { public static readonly ParameterInstances Empty = new([]); @@ -11,19 +13,17 @@ public class ParameterInstances : IEquatable, IDisposable public ParameterInstance this[int index] => Items[index]; public object? this[string name] => Items.FirstOrDefault(item => item.Name == name)?.Value; - private string? printInfo = null; - public ParameterInstances(IReadOnlyList items) { Items = items; } + public ValueTask DisposeAsync() => Items.DisposeAllAsync(); + public void Dispose() { - foreach (var parameterInstance in Items) - { - parameterInstance.Dispose(); - } + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + context.ExecuteUntilComplete(DisposeAsync()); } public string FolderInfo => string.Join("_", Items.Select(p => $"{p.Name}-{p.ToDisplayText()}")).AsValidFileName(); @@ -32,45 +32,8 @@ public void Dispose() public string ValueInfo => Items.Any() ? "[" + string.Join(", ", Items.Select(p => $"{p.Name}={p.Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation}")) + "]" : ""; - public string PrintInfo => printInfo ?? (printInfo = string.Join("&", Items.Select(p => $"{p.Name}={p.ToDisplayText()}"))); + public string PrintInfo => field ??= string.Join("&", Items.Select(p => $"{p.Name}={p.ToDisplayText()}")); public ParameterInstance GetArgument(string name) => Items.Single(parameter => parameter.IsArgument && parameter.Name == name); - - public bool Equals(ParameterInstances? other) - { - if (ReferenceEquals(this, other)) - return true; - - if (other == null) - return false; - - if (other.Count != Count) - return false; - - if (Count != other.Count) - return false; - - for (int i = 0; i < Count; i++) - { - var currentItem = Items[i]; - var otherItem = other.Items[i]; - - if (ReferenceEquals(currentItem, otherItem)) - continue; - - if (currentItem is null || otherItem is null) - return false; - - if (!currentItem.Equals(otherItem.Value)) - return false; - } - - return true; - } - - public override bool Equals(object? obj) - => obj is ParameterInstances other && Equals(other); - - public override int GetHashCode() => FolderInfo.GetHashCode(); } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Parameters/ParameterValue.cs b/src/BenchmarkDotNet/Parameters/ParameterValue.cs new file mode 100644 index 0000000000..c8c404ff1f --- /dev/null +++ b/src/BenchmarkDotNet/Parameters/ParameterValue.cs @@ -0,0 +1,39 @@ +namespace BenchmarkDotNet.Parameters; + +/// +/// One value a benchmark parameter can take, and a language-neutral description for a toolchain to re-create it in generated code. +/// +public abstract class ParameterValue +{ + /// The value of the parameter. + public object? Value { get; } + + private ParameterValue(object? value) => Value = value; + + /// A value the toolchain can embed directly, e.g. a primitive, string, enum, array, or . + public sealed class Constant(object? value, Type type) : ParameterValue(value) + { + /// The declared type of the parameter. + /// + /// Needed because the value alone can be ambiguous - an enum declared in F# is erased to its underlying type in attribute metadata (dotnet/fsharp#995). + /// + public Type Type { get; } = type; + } + + /// + /// A value the toolchain cannot embed, so the generated code re-obtains it by enumerating the [ParamsSource]/[ArgumentsSource] member it originally came from. + /// + public sealed class FromSource(object? value, SourceRead read, int? elementIndex, Type targetType) : ParameterValue(value) + { + /// The read this value comes out of, shared by every parameter bound from the same one. + public SourceRead Read { get; } = read; + + /// + /// The index of the value within the yielded element, if the element is an args-list array; otherwise. + /// + public int? ElementIndex { get; } = elementIndex; + + /// The type the obtained value is used as. + public Type TargetType { get; } = targetType; + } +} diff --git a/src/BenchmarkDotNet/Parameters/ParameterValues.cs b/src/BenchmarkDotNet/Parameters/ParameterValues.cs new file mode 100644 index 0000000000..de8d19fc44 --- /dev/null +++ b/src/BenchmarkDotNet/Parameters/ParameterValues.cs @@ -0,0 +1,17 @@ +namespace BenchmarkDotNet.Parameters +{ + /// + /// A parameter and the values it takes; the benchmark cases are the cartesian product of these. + /// + /// + /// Only a [Params], [ParamsSource] or [ParamsAllValues] member ranges over values this way. An argument is + /// filled from a row spanning the whole parameter list, so its values belong to the row and there is nothing + /// to pair with the definition. + /// + public sealed class ParameterValues(ParameterDefinition definition, IReadOnlyList items) + { + public ParameterDefinition Definition { get; } = definition; + + public IReadOnlyList Items { get; } = items; + } +} diff --git a/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs b/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs index d204fb8e62..a23ed1b6f6 100644 --- a/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs +++ b/src/BenchmarkDotNet/Parameters/SmartParamBuilder.cs @@ -1,188 +1,148 @@ -using BenchmarkDotNet.Code; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using JetBrains.Annotations; using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace BenchmarkDotNet.Parameters { internal static class SmartParamBuilder { - [SuppressMessage("ReSharper", "CoVariantArrayConversion")] - internal static object[] CreateForParams(Type parameterType, MemberInfo source, object[] values) + internal static IReadOnlyList CreateForParams(Type parameterType, MemberInfo source, object?[] values) { - // IEnumerable - if (values.IsEmpty() || values.All(SourceCodeHelper.IsCompilationTimeConstant)) - return values; - - // IEnumerable - if (values.All(value => value is object[] array && array.Length == 1 && SourceCodeHelper.IsCompilationTimeConstant(array[0]))) - return values.Select(x => ((object[])x)[0]).ToArray(); - - return values.Select((value, index) => new SmartParameter(parameterType, source, value, index)).ToArray(); + // A one-element object[] around a constant is unwrapped to the constant, which is then rendered inline + // and needs no index. Only around a constant: a value that has to be read back from the source keeps + // the whole array, because the generated code emits no index here to reach inside one with, and + // unwrapping only the in-process side would have the two toolchains assign different values. + if (values.All(value => value is object[] { Length: 1 } wrapper && SourceCodeHelper.IsCompilationTimeConstant(wrapper[0]))) + values = values.Select(value => ((object[]) value!)[0]).ToArray(); + + return values.Select((value, index) => + SourceCodeHelper.IsCompilationTimeConstant(value) + ? (ParameterValue) new ParameterValue.Constant(value, parameterType) + : new ParameterValue.FromSource(value, new SourceRead(source, index), elementIndex: null, parameterType)).ToArray(); } - internal static ParameterInstances CreateForArguments(MethodInfo benchmark, ParameterDefinition[] parameterDefinitions, (MemberInfo source, object[] values) valuesInfo, int sourceIndex, SummaryStyle summaryStyle) + internal static ParameterInstances CreateForArguments( + MethodInfo benchmark, + ParameterDefinition[] parameterDefinitions, + (MemberInfo source, object?[] values) valuesInfo, + int sourceIndex, + SummaryStyle summaryStyle) { var unwrappedValue = valuesInfo.values[sourceIndex]; + // One read for the whole row: every parameter below takes its value out of this one, so the generated + // code enumerates the source once per case rather than once per argument. + var read = new SourceRead(valuesInfo.source, sourceIndex); + if (unwrappedValue is object[] array) { - Type? firstParamType = benchmark.GetParameters().FirstOrDefault()?.ParameterType; - // the user provided object[] for a benchmark accepting a single argument - if (parameterDefinitions.Length == 1 && array.Length == 1 - && (array[0]?.GetType() == firstParamType || (firstParamType != null && firstParamType.IsStackOnlyWithImplicitCast(array[0])))) // the benchmark that accepts an object[] as argument + var firstParameterType = parameterDefinitions.FirstOrDefault()?.ParameterType; + + // An object[] for a benchmark taking a single argument: the one that takes the array itself, or a + // by-ref-like one built from it. + if (parameterDefinitions.Length == 1 && array.Length == 1 && firstParameterType is not null + && (array[0]?.GetType() == firstParameterType || firstParameterType.IsStackOnlyWithImplicitCast(array[0]))) { return new ParameterInstances( - [Create(parameterDefinitions, array[0], valuesInfo.source, sourceIndex, argumentIndex: 0, summaryStyle)]); + [Create(parameterDefinitions, array[0], read, argumentIndex: 0, summaryStyle)]); } if (parameterDefinitions.Length > 1) { if (parameterDefinitions.Length != array.Length) - throw new InvalidOperationException($"Benchmark {benchmark.Name} has invalid number of arguments provided by [ArgumentsSource({valuesInfo.source.Name})]! {array.Length} instead of {parameterDefinitions.Length}."); + throw new InvalidOperationException($"Benchmark {benchmark.Name} has invalid number of arguments provided by [ArgumentsSource({valuesInfo.source.Name})]!" + + $" {array.Length} instead of {parameterDefinitions.Length}."); return new ParameterInstances( - array.Select((value, argumentIndex) => Create(parameterDefinitions, value, valuesInfo.source, sourceIndex, argumentIndex, summaryStyle)).ToArray()); + array.Select((value, argumentIndex) => Create(parameterDefinitions, value, read, argumentIndex, summaryStyle)) + .ToArray()); } } if (parameterDefinitions.Length == 1) { - return new ParameterInstances([Create(parameterDefinitions, unwrappedValue, valuesInfo.source, sourceIndex, argumentIndex: 0, summaryStyle)]); + return new ParameterInstances([Create(parameterDefinitions, unwrappedValue, read, argumentIndex: 0, summaryStyle)]); } - throw new NotSupportedException($"Benchmark {benchmark.Name} has invalid type of arguments provided by [ArgumentsSource({valuesInfo.source.Name})]. It should be IEnumerable or IEnumerable."); + throw new NotSupportedException($"Benchmark {benchmark.Name} has invalid type of arguments provided by [ArgumentsSource({valuesInfo.source.Name})]." + + $" It should be IEnumerable, IEnumerable, IAsyncEnumerable or IAsyncEnumerable."); } - private static ParameterInstance Create(ParameterDefinition[] parameterDefinitions, object value, MemberInfo source, int sourceIndex, int argumentIndex, SummaryStyle summaryStyle) + // Whether the generated code indexes the element it extracts, decided by the interface the source is + // *written as* and not by which branch above produced this instance: the extraction call is bound against + // the declared return type, so the index has to be read from the same place the binding is. + private static bool Indexes(MemberInfo source) { - if (SourceCodeHelper.IsCompilationTimeConstant(value)) - return new ParameterInstance(parameterDefinitions[argumentIndex], value, summaryStyle); + var returnType = source.GetSourceReturnType(); - return new ParameterInstance(parameterDefinitions[argumentIndex], new SmartArgument(parameterDefinitions, value, source, sourceIndex, argumentIndex), summaryStyle); - } - } - - internal class SmartArgument : IParam - { - private readonly ParameterDefinition[] parameterDefinitions; - private readonly MemberInfo source; - private readonly int sourceIndex; - private readonly int argumentIndex; - - public SmartArgument(ParameterDefinition[] parameterDefinitions, object value, MemberInfo source, int sourceIndex, int argumentIndex) - { - this.parameterDefinitions = parameterDefinitions; - Value = value; - this.source = source; - this.sourceIndex = sourceIndex; - this.argumentIndex = argumentIndex; + return returnType == typeof(IEnumerable) || returnType == typeof(IAsyncEnumerable); } - public object Value { get; } - - public string DisplayText => Value is Array array ? ArrayParam.GetDisplayString(array) : Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation; - - public string ToSourceCode() + private static ParameterInstance Create(ParameterDefinition[] parameterDefinitions, object? value, SourceRead read, int argumentIndex, SummaryStyle summaryStyle) { - Type paramType = parameterDefinitions[argumentIndex].ParameterType; - - // it's an object so we need to cast it to the right type - string cast = paramType.IsByRefLike() - ? $"({Value.GetType().GetCorrectCSharpTypeName()})" - : $"({paramType.GetCorrectCSharpTypeName()})"; - - string callPostfix = source is PropertyInfo ? string.Empty : "()"; - - MethodInfo? sourceAsMethodInfo = source as MethodInfo; - PropertyInfo? sourceAsPropertyInfo = source as PropertyInfo; - - Type indexableType = typeof(IEnumerable); - - string indexPostfix; - if (sourceAsMethodInfo?.ReturnType == indexableType || - sourceAsPropertyInfo?.GetMethod?.ReturnType == indexableType) - { - indexPostfix = $"[{argumentIndex}]"; - } - else - { - indexPostfix = string.Empty; // IEnumerable - } + var definition = parameterDefinitions[argumentIndex]; + + // Asked ahead of the constant path, which renders the value into the source and needs the conversion + // just as much. Null is left to it: a ref struct is not written as one, and how that is rendered is + // settled there. + var takesByRefLike = definition.ParameterType.WithoutRefModifier(); + + // InvalidBenchmarkDeclarationException, not InvalidOperationException: BenchmarkRunnerDirty catches this + // one and reports it as that benchmark's summary, where anything else escapes Run and takes every other + // benchmark in the call down with it. + if (value is not null && takesByRefLike.IsByRefLike() && !takesByRefLike.IsStackOnlyWithImplicitCast(value)) + throw new InvalidBenchmarkDeclarationException($"[ArgumentsSource({read.Source.Name})] provides a {value!.GetType().GetDisplayName()}" + + $" for the {definition.ParameterType.GetDisplayName()} parameter '{definition.Name}', which has no implicit conversion from it." + + $" A by-ref-like parameter only ever takes its value through that conversion, so nothing can be cast to it here." + + $" Please, yield a type it converts from - and where the source's element type is a type parameter," + + $" the [GenericTypeArguments] in play decide this, so it can hold for one and not the next."); - string methodCall; - if (sourceAsMethodInfo?.IsStatic ?? sourceAsPropertyInfo?.GetMethod?.IsStatic ?? throw new Exception($"{nameof(source)} was not {nameof(MethodInfo)} nor {nameof(PropertyInfo)}")) - { - // If the source member is static, we need to place the fully qualified type name before it, in case the source member is from another type that this generated type does not inherit from. - methodCall = $"{source.DeclaringType!.GetCorrectCSharpTypeName()}.{source.Name}"; - } - else - { - // If the source member is non-static, we mustn't include the type name, as this would be a compiler error when accessing a non-static source member in the base class of this generated type. - methodCall = $"base.{source.Name}"; - } - - // we do something like enumerable.ElementAt(sourceIndex)[argumentIndex]; - return $"{cast}BenchmarkDotNet.Parameters.ParameterExtractor.GetParameter({methodCall}{callPostfix}, {sourceIndex}){indexPostfix};"; - } - } - - internal class SmartParameter : IParam - { - private readonly Type parameterType; - private readonly MemberInfo source; - private readonly MethodBase method; - private readonly int index; - - public SmartParameter(Type parameterType, MemberInfo source, object value, int index) - { - this.parameterType = parameterType; - this.source = source; - method = source is PropertyInfo property ? property.GetMethod! : (MethodInfo)source; - Value = value; - this.index = index; - } - - public object Value { get; } - - public string DisplayText => Value is Array array ? ArrayParam.GetDisplayString(array) : Value?.ToString() ?? ParameterInstance.NullParameterTextRepresentation; - - public string ToSourceCode() - { - string cast = $"({parameterType.GetCorrectCSharpTypeName()})"; // it's an object so we need to cast it to the right type - - string callPrefix = method.IsStatic ? source.DeclaringType!.GetCorrectCSharpTypeName() : "base"; + if (SourceCodeHelper.IsCompilationTimeConstant(value)) + return new ParameterInstance(definition, new ParameterValue.Constant(value, definition.ParameterType), summaryStyle); - string callPostfix = source is PropertyInfo ? string.Empty : "()"; + // A by-ref-like parameter can't be the source's element type, so the value's own type is the one the + // generated code casts to, relying on its implicit conversion (#774). + // value is non-null here: null is a compilation-time constant, handled above. + var targetType = takesByRefLike.IsByRefLike() ? value!.GetType() : definition.ParameterType; - // we so something like enumerable.ElementAt(index); - return $"{cast}BenchmarkDotNet.Parameters.ParameterExtractor.GetParameter({callPrefix}.{source.Name}{callPostfix}, {index});"; + return new ParameterInstance( + definition, + new ParameterValue.FromSource(value, read, Indexes(read.Source) ? argumentIndex : null, targetType), + summaryStyle); } } + [EditorBrowsable(EditorBrowsableState.Never)] + [UsedImplicitly] public static class ParameterExtractor { - [EditorBrowsable(EditorBrowsableState.Never)] // hide from intellisense, it's public so we can call it form the boilerplate code - public static T GetParameter(IEnumerable parameters, int index) + public static ValueTask GetParameterAsync(IEnumerable parameters, int index, CancellationToken cancellationToken) + => GetParameterAsync(parameters.ToAsyncEnumerable(), index, cancellationToken); + + public static async ValueTask GetParameterAsync(IAsyncEnumerable parameters, int index, CancellationToken cancellationToken) { int count = 0; - foreach (T parameter in parameters) +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await foreach (T parameter in parameters.ConfigureAwait(cancellationToken)) +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task { if (count == index) { return parameter; } - if (parameter is IDisposable disposable) + // #1383 + if (parameter is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync().ConfigureAwait(); + } + else if (parameter is IDisposable disposable) { - // parameters might contain locking finalizers which might cause the benchmarking process to hung at the end - // to avoid that, we dispose the parameters that were created, but won't be used - // (for every test case we have to enumerate the underlying source enumerator and stop when we reach index of given test case) - // See https://github.com/dotnet/BenchmarkDotNet/issues/1383 and https://github.com/dotnet/runtime/issues/314 for more disposable.Dispose(); } diff --git a/src/BenchmarkDotNet/Parameters/SourceRead.cs b/src/BenchmarkDotNet/Parameters/SourceRead.cs new file mode 100644 index 0000000000..f24a03268f --- /dev/null +++ b/src/BenchmarkDotNet/Parameters/SourceRead.cs @@ -0,0 +1,22 @@ +using System.Reflection; + +namespace BenchmarkDotNet.Parameters; + +/// +/// One read of a [ParamsSource]/[ArgumentsSource] member: the member, and which of the values it yields. +/// +/// +/// An arguments row is a single read that every parameter in it takes a value out of, so they share one instance +/// of this and differ only in . A toolchain can therefore emit +/// the read once and index into it, instead of recognising after the fact that several parameters would have +/// re-read the same member at the same index. A [ParamsSource] value is a read of its own: members range over +/// their values independently, so nothing is shared between them. +/// +public sealed class SourceRead(MemberInfo source, int valueIndex) +{ + /// The source member (method or property) to enumerate. + public MemberInfo Source { get; } = source; + + /// Index of the value within the sequence the source yields. + public int ValueIndex { get; } = valueIndex; +} diff --git a/src/BenchmarkDotNet/Running/BenchmarkCase.cs b/src/BenchmarkDotNet/Running/BenchmarkCase.cs index 4633ecf4f6..1d7c9ebc7e 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkCase.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkCase.cs @@ -6,7 +6,7 @@ namespace BenchmarkDotNet.Running { - public class BenchmarkCase : IComparable, IDisposable + public class BenchmarkCase : IComparable, IDisposable, IAsyncDisposable { public Descriptor Descriptor { get; } public Job Job { get; } @@ -30,6 +30,8 @@ public Runtime GetRuntime() => Job.Environment.HasValue(EnvironmentMode.RuntimeC ? Job.Environment.Runtime! : RuntimeInformation.GetTargetOrCurrentRuntime(Descriptor.Type.Assembly); + public ValueTask DisposeAsync() => Parameters.DisposeAsync(); + public void Dispose() => Parameters.Dispose(); public int CompareTo(BenchmarkCase? other) diff --git a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs index bcc40e93ad..77ca206be2 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkConverter.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkConverter.cs @@ -1,8 +1,10 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Code; using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Filters; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Parameters; using BenchmarkDotNet.Reports; using System.Collections; @@ -16,6 +18,12 @@ public static class BenchmarkConverter private const BindingFlags AllMethodsFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; public static BenchmarkRunInfo TypeToBenchmarks(Type type, IConfig? config = null) + { + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(TypeToBenchmarksAsync(type, config)); + } + + public static ValueTask TypeToBenchmarksAsync(Type type, IConfig? config = null, CancellationToken cancellationToken = default) { if (type.IsGenericTypeDefinition) throw new InvalidBenchmarkDeclarationException($"{type.Name} is generic type definition, use BenchmarkSwitcher for it"); // for "open generic types" should be used BenchmarkSwitcher @@ -23,11 +31,17 @@ public static BenchmarkRunInfo TypeToBenchmarks(Type type, IConfig? config = nul // We should check all methods including private to notify users about private methods with the [Benchmark] attribute var benchmarkMethods = GetOrderedBenchmarkMethods(type.GetMethods(AllMethodsFlags)); - return MethodsToBenchmarksWithFullConfig(type, benchmarkMethods, config); + return MethodsToBenchmarksWithFullConfig(type, benchmarkMethods, config, cancellationToken); } public static BenchmarkRunInfo MethodsToBenchmarks(Type containingType, MethodInfo[] benchmarkMethods, IConfig? config = null) - => MethodsToBenchmarksWithFullConfig(containingType, GetOrderedBenchmarkMethods(benchmarkMethods), config); + { + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(MethodsToBenchmarksAsync(containingType, benchmarkMethods, config)); + } + + public static ValueTask MethodsToBenchmarksAsync(Type containingType, MethodInfo[] benchmarkMethods, IConfig? config = null, CancellationToken cancellationToken = default) + => MethodsToBenchmarksWithFullConfig(containingType, GetOrderedBenchmarkMethods(benchmarkMethods), config, cancellationToken); private static MethodInfo[] GetOrderedBenchmarkMethods(MethodInfo[] methods) => methods @@ -38,7 +52,7 @@ private static MethodInfo[] GetOrderedBenchmarkMethods(MethodInfo[] methods) .Select(pair => pair.method) .ToArray(); - private static BenchmarkRunInfo MethodsToBenchmarksWithFullConfig(Type type, MethodInfo[] benchmarkMethods, IConfig? config) + private static async ValueTask MethodsToBenchmarksWithFullConfig(Type type, MethodInfo[] benchmarkMethods, IConfig? config, CancellationToken cancellationToken) { var allMethods = type.GetMethods(AllMethodsFlags); // benchmarkMethods can be filtered, without Setups, look #564 var configPerType = GetFullTypeConfig(type, config); @@ -51,8 +65,7 @@ private static BenchmarkRunInfo MethodsToBenchmarksWithFullConfig(Type type, Met var targets = GetTargets(benchmarkMethods, type, globalSetupMethods, globalCleanupMethods, iterationSetupMethods, iterationCleanupMethods, configPerType).ToArray(); - var parameterDefinitions = GetParameterDefinitions(type); - var parameterInstancesList = parameterDefinitions.Expand(configPerType.SummaryStyle); + var parameterInstances = await GetParameterInstancesAsync(type, configPerType.SummaryStyle, cancellationToken).ConfigureAwait(); var benchmarks = new List(); @@ -60,26 +73,26 @@ private static BenchmarkRunInfo MethodsToBenchmarksWithFullConfig(Type type, Met foreach (var target in targets) { - var argumentsDefinitions = GetArgumentsDefinitions(target.WorkloadMethod, target.Type, configPerType.SummaryStyle).ToArray(); + var argumentsInstances = await GetArgumentsInstancesAsync(target.WorkloadMethod, target.Type, configPerType.SummaryStyle, cancellationToken).ConfigureAwait(); - var parameterInstances = - (from parameterInstance in parameterInstancesList - from argumentDefinition in argumentsDefinitions - select new ParameterInstances(parameterInstance.Items.Concat(argumentDefinition.Items).ToArray())).ToArray(); + var targetParameterInstances = + (from parameterInstance in parameterInstances + from argumentInstance in argumentsInstances + select new ParameterInstances([.. parameterInstance.Items, .. argumentInstance.Items])).ToArray(); var configPerMethod = GetFullMethodConfig(target.WorkloadMethod, configPerType); var benchmarksForTarget = - from job in configPerMethod.GetJobs() - from parameterInstance in parameterInstances - select BenchmarkCase.Create(target, job, parameterInstance, configPerMethod); + (from job in configPerMethod.GetJobs() + from parameterInstance in targetParameterInstances + select BenchmarkCase.Create(target, job, parameterInstance, configPerMethod)).ToArray(); - if (benchmarksForTarget.Any() && !containsBenchmarkDeclarations) containsBenchmarkDeclarations = true; + containsBenchmarkDeclarations |= benchmarksForTarget.Length != 0; benchmarks.AddRange(GetFilteredBenchmarks(benchmarksForTarget, configPerMethod.GetFilters())); } - var orderedBenchmarks = configPerType.Orderer.GetExecutionOrder(benchmarks.ToImmutableArray()).ToArray(); + var orderedBenchmarks = configPerType.Orderer.GetExecutionOrder([.. benchmarks]).ToArray(); var compositeInProcessDiagnoser = new Diagnosers.CompositeInProcessDiagnoser([.. configPerType.GetDiagnosers().OfType()]); return new BenchmarkRunInfo(orderedBenchmarks, type, configPerType, containsBenchmarkDeclarations, compositeInProcessDiagnoser); @@ -179,84 +192,98 @@ private static Descriptor CreateDescriptor( return target; } - private static ParameterDefinitions GetParameterDefinitions(Type type) + private static async ValueTask> GetParameterInstancesAsync(Type type, SummaryStyle summaryStyle, CancellationToken cancellationToken) { - IEnumerable GetDefinitions(Func getValidValues) where TAttribute : PriorityAttribute + IEnumerable GetValues(Func> getValidValues) where TAttribute : PriorityAttribute + => type.GetTypeMembersWithGivenAttribute(ReflectionExtensions.ParameterMemberFlags) + .Select(member => + new ParameterValues( + new(member.Name, member.IsStatic, isArgument: false, member.ParameterType, member.Attribute.Priority), + getValidValues(member.Attribute, member.ParameterType) + ) + ); + + var parameters = GetValues((attribute, parameterType) => GetValidValues(attribute.Values, parameterType)).ToList(); + foreach (var member in type.GetTypeMembersWithGivenAttribute(ReflectionExtensions.ParameterMemberFlags)) { - const BindingFlags reflectionFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - - var allMembers = type.GetTypeMembersWithGivenAttribute(reflectionFlags); - return allMembers.Select(member => - new ParameterDefinition( - member.Name, - member.IsStatic, - getValidValues(member.Attribute, member.ParameterType), - false, - member.ParameterType, - member.Attribute.Priority)); + var targetType = member.Attribute.Type ?? type; + var (source, values) = await GetValidValuesForParamsSourceAsync(targetType, member.Attribute.Name, cancellationToken).ConfigureAwait(); + parameters.Add(new ParameterValues( + new ParameterDefinition(member.Name, member.IsStatic, isArgument: false, member.ParameterType, member.Attribute.Priority), + SmartParamBuilder.CreateForParams(member.ParameterType, source, values))); } + parameters.AddRange(GetValues((_, parameterType) => GetValidValues(GetAllValidValues(parameterType), parameterType))); - var paramsDefinitions = GetDefinitions((attribute, parameterType) => GetValidValues(attribute.Values, parameterType)); - - var paramsSourceDefinitions = GetDefinitions((attribute, parameterType) => + // Each member ranges over its values independently, so the cases are their cartesian product: every case so far is re-made once per value + // of the next parameter. The seed is the single empty case, which is also the answer for a benchmark that has no parameters at all. + List cases = [ParameterInstances.Empty]; + List? expanded = null; + foreach (var parameter in parameters) { - var targetType = attribute.Type ?? type; - - var paramsValues = GetValidValuesForParamsSource(targetType, attribute.Name); - return SmartParamBuilder.CreateForParams(parameterType, paramsValues.source, paramsValues.values); - }); - - var paramsAllValuesDefinitions = GetDefinitions((_, parameterType) => GetAllValidValues(parameterType)); + expanded ??= []; + expanded.Clear(); +#if NET6_0_OR_GREATER + expanded.EnsureCapacity(cases.Count * parameter.Items.Count); +#endif + foreach (var instances in cases) + { + foreach (var value in parameter.Items) + { + expanded.Add(new ParameterInstances([.. instances.Items, new(parameter.Definition, value, summaryStyle)])); + } + } + (cases, expanded) = (expanded, cases); + } - var definitions = paramsDefinitions.Concat(paramsSourceDefinitions).Concat(paramsAllValuesDefinitions).ToArray(); - return new ParameterDefinitions(definitions); + return cases; } - private static IEnumerable GetArgumentsDefinitions(MethodInfo benchmark, Type benchmarkType, SummaryStyle summaryStyle) + private static async ValueTask> GetArgumentsInstancesAsync(MethodInfo benchmark, Type benchmarkType, SummaryStyle summaryStyle, CancellationToken cancellationToken) { - var argumentsAttributes = benchmark.GetCustomAttributes(); - int priority = argumentsAttributes.Select(attribute => attribute.Priority).Sum(); + int priority = benchmark.GetCustomAttributes().Sum(attribute => attribute.Priority); var parameterDefinitions = benchmark.GetParameters() - .Select(parameter => new ParameterDefinition(parameter.Name!, false, [], true, parameter.ParameterType, priority)) + .Select(parameter => new ParameterDefinition(parameter.Name!, isStatic: false, isArgument: true, parameter.ParameterType, priority)) .ToArray(); - if (parameterDefinitions.IsEmpty()) + if (parameterDefinitions.Length == 0) { - yield return new ParameterInstances([]); - yield break; + return [ParameterInstances.Empty]; } + var result = new List(); foreach (var argumentsAttribute in benchmark.GetCustomAttributes()) { if (parameterDefinitions.Length != argumentsAttribute.Values.Length) throw new InvalidOperationException($"Benchmark {benchmark.Name} has invalid number of defined arguments provided with [Arguments]! {argumentsAttribute.Values.Length} instead of {parameterDefinitions.Length}."); - yield return new ParameterInstances( - argumentsAttribute - .Values + result.Add( + new(argumentsAttribute.Values .Select((value, index) => - { - var definition = parameterDefinitions[index]; - var type = definition.ParameterType; - return new ParameterInstance(definition, Map(value, type), summaryStyle); - }) - .ToArray()); + { + var definition = parameterDefinitions[index]; + return new ParameterInstance(definition, new ParameterValue.Constant(value, definition.ParameterType), summaryStyle); + }) + .ToArray() + ) + ); } if (!benchmark.HasAttribute()) - yield break; + return result; var argumentsSourceAttribute = benchmark.GetCustomAttribute()!; var targetType = argumentsSourceAttribute.Type ?? benchmarkType; - var valuesInfo = GetValidValuesForParamsSource(targetType, argumentsSourceAttribute.Name); + var valuesInfo = await GetValidValuesForParamsSourceAsync(targetType, argumentsSourceAttribute.Name, cancellationToken).ConfigureAwait(); for (int sourceIndex = 0; sourceIndex < valuesInfo.values.Length; sourceIndex++) - yield return SmartParamBuilder.CreateForArguments(benchmark, parameterDefinitions, valuesInfo, sourceIndex, summaryStyle); + result.Add(SmartParamBuilder.CreateForArguments(benchmark, parameterDefinitions, valuesInfo, sourceIndex, summaryStyle)); + + return result; } - private static ImmutableArray GetFilteredBenchmarks(IEnumerable benchmarks, IEnumerable filters) - => benchmarks.Where(benchmark => filters.All(filter => filter.Predicate(benchmark))).ToImmutableArray(); + private static ImmutableArray GetFilteredBenchmarks(BenchmarkCase[] benchmarks, IEnumerable filters) + => [.. benchmarks.Where(benchmark => filters.All(filter => filter.Predicate(benchmark)))]; private static void AssertMethodHasCorrectSignature(string methodType, MethodInfo methodInfo) { @@ -277,57 +304,93 @@ private static void AssertMethodIsNotGeneric(string methodType, MethodInfo metho throw new InvalidBenchmarkDeclarationException($"{methodType} method {methodInfo.Name} is generic.\nGeneric {methodType} methods are not supported."); } - private static object?[] GetValidValues(object?[] values, Type parameterType) - => values.Select(value => Map(value, parameterType)).ToArray(); + private static IReadOnlyList GetValidValues(object?[] values, Type parameterType) + => [.. values.Select(value => new ParameterValue.Constant(value, parameterType))]; - private static object? Map(object? providedValue, Type type) + private static async ValueTask<(MemberInfo source, object?[] values)> GetValidValuesForParamsSourceAsync(Type sourceType, string sourceName, CancellationToken cancellationToken) { - if (providedValue == null) - return null; + var source = sourceType.FindSourceMember(sourceName); - if (providedValue.GetType().IsArray) - { - return ArrayParam.FromObject(providedValue); - } - // Usually providedValue contains all needed type information, - // but in case of F# enum types in attributes are erased. - // We can to restore them from types of arguments and fields. - // See also: - // https://github.com/dotnet/fsharp/issues/995 - else if (providedValue.GetType().IsEnum || type.IsEnum) - { - return EnumParam.FromObject(providedValue, type); - } - return providedValue; + if (source == null) + throw NoSourceMemberFound(sourceType, sourceName); + + // A source method may have parameters as long as they are all optional (e.g. an async iterator with an + // [EnumeratorCancellation] CancellationToken); we invoke it with their default values. + object? sourceValue = source is MethodInfo method + ? method.Invoke(method.IsStatic ? null : Activator.CreateInstance(sourceType), GetDefaultArguments(method)) + : ((PropertyInfo) source).GetValue(((PropertyInfo) source).GetMethod!.IsStatic ? null : Activator.CreateInstance(sourceType)!); + + return (source, await ToArrayAsync(sourceValue, source, sourceType, cancellationToken).ConfigureAwait()); } - private static (MemberInfo source, object[] values) GetValidValuesForParamsSource(Type sourceType, string sourceName) + private static InvalidBenchmarkDeclarationException NoSourceMemberFound(Type sourceType, string sourceName) { - var paramsSourceMethod = sourceType.GetAllMethods().FirstOrDefault(method => method.Name == sourceName && method.IsPublic); + var namedMethods = sourceType.GetAllMethods().Where(method => method.Name == sourceName && method.IsPublic).ToArray(); - if (paramsSourceMethod != default) - return (paramsSourceMethod, ToArray( - paramsSourceMethod.Invoke(paramsSourceMethod.IsStatic ? null : Activator.CreateInstance(sourceType), null)!, - paramsSourceMethod, - sourceType)); + if (namedMethods.Any(method => method.IsGenericMethodDefinition)) + return new InvalidBenchmarkDeclarationException($"Source method {sourceName} of type {sourceType.GetDisplayName()} is generic.\nGeneric source methods are not supported."); - var paramsSourceProperty = sourceType.GetAllProperties().FirstOrDefault(property => property.Name == sourceName && property.GetMethod?.IsPublic == true); + return namedMethods.Length > 0 + ? new InvalidBenchmarkDeclarationException($"{sourceType.Name}.{sourceName} has required parameters, unable to read values for [ParamsSource]/[ArgumentsSource]. A source method must be parameterless or have only optional parameters.") + : new InvalidBenchmarkDeclarationException($"{sourceType.Name} has no public, accessible method/property called {sourceName}, unable to read values for [ParamsSource]."); + } - if (paramsSourceProperty == null) - throw new InvalidBenchmarkDeclarationException($"{sourceType.Name} has no public, accessible method/property called {sourceName}, unable to read values for [ParamsSource]"); + // Default argument values for an all-optional-parameter source method. A parameter can be optional without declaring + // a default ([Optional] with no [DefaultParameterValue]), and MethodInfo.Invoke(object, object[]) does no optional-parameter + // binding - so we pass default(T), which is what the C# compiler passes at a call site that omits the argument. + private static object?[]? GetDefaultArguments(MethodInfo method) + { + var parameters = method.GetParameters(); + if (parameters.Length == 0) + return null; - return (paramsSourceProperty, ToArray( - paramsSourceProperty.GetValue(paramsSourceProperty.GetMethod!.IsStatic ? null : Activator.CreateInstance(sourceType)!)!, - paramsSourceProperty, - sourceType)); + var arguments = new object?[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + arguments[i] = parameters[i].GetDefaultArgumentValue(); + return arguments; } - private static object[] ToArray(object sourceValue, MemberInfo memberInfo, Type type) + private static async ValueTask ToArrayAsync(object? sourceValue, MemberInfo memberInfo, Type type, CancellationToken cancellationToken) { - if (!(sourceValue is IEnumerable collection)) - throw new InvalidBenchmarkDeclarationException($"{memberInfo.Name} of type {type.Name} does not implement IEnumerable, unable to read values for [ParamsSource]"); + var sourceType = memberInfo is MethodInfo methodInfo + ? methodInfo.ReturnType + : ((PropertyInfo) memberInfo).PropertyType; + + // Checked before the shape, so a null async source reports the same declaration error a null + // synchronous source does instead of failing while being enumerated. + if (sourceValue == null) + throw new InvalidBenchmarkDeclarationException($"{memberInfo.Name} of type {type.Name} returned null, unable to read values for [ParamsSource]/[ArgumentsSource]."); + + // Reading the values puts each into an object[], which a ref struct cannot enter - the enumeration fails + // inside reflection saying nothing about the benchmark. Expressible since .NET 10 gave IEnumerable an + // allows-ref-struct type parameter. Asked of both shapes, and so ahead of either: an async source reads + // its values into the same object[]. SourceReturnTypeValidator reports the declaration this substitutes. + if (memberInfo.GetSourceReturnType().TryGetSourceElementType(out var refLikeCandidate) && refLikeCandidate.IsByRefLike()) + throw new InvalidBenchmarkDeclarationException( + $"{type.Name}.{memberInfo.Name} yields {refLikeCandidate.GetDisplayName()}, which is a ref struct, and BenchmarkDotNet cannot read a value into one." + + " Please, yield what the value is built from - IEnumerable for a ReadOnlySpan parameter - and let the benchmark take the ref struct."); + + // Only IAsyncEnumerable is supported for async sources (not the await-foreach pattern). Decided from + // the declared type, and before the synchronous check, because that is what the generated code binds: + // an async-declared source whose value also implements IEnumerable must not be read synchronously here. + if (sourceType.IsIAsyncEnumerable(out var elementType)) + { + List items = []; +#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task + await foreach (var item in DynamicAwaitHelper.EnumerateSourceAsync(sourceValue, elementType).ConfigureAwait(cancellationToken)) +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task + { + items.Add(item); + } + return [.. items]; + } + + // Synchronous sources are matched on the value: the declared type is often looser than what is returned + // (e.g. a non-generic IEnumerable), and the generated code binds the IEnumerable overload either way. + if (sourceValue is IEnumerable collection) + return [.. collection]; - return collection.Cast().ToArray(); + throw new InvalidBenchmarkDeclarationException($"{memberInfo.Name} of type {type.Name} does not implement IEnumerable or IAsyncEnumerable, unable to read values for [ParamsSource]"); } private static object?[] GetAllValidValues(Type parameterType) @@ -338,16 +401,16 @@ private static object[] ToArray(object sourceValue, MemberInfo memberInfo, Type if (parameterType.GetTypeInfo().IsEnum) { if (parameterType.GetTypeInfo().IsDefined(typeof(FlagsAttribute))) - return [Activator.CreateInstance(parameterType)!]; + return [Activator.CreateInstance(parameterType)]; - return Enum.GetValues(parameterType).Cast().ToArray(); + return [.. Enum.GetValues(parameterType).Cast()]; } var nullableUnderlyingType = Nullable.GetUnderlyingType(parameterType); if (nullableUnderlyingType != null) - return new object?[] { null }.Concat(GetAllValidValues(nullableUnderlyingType)).ToArray(); + return [null, .. GetAllValidValues(nullableUnderlyingType)]; - return [Activator.CreateInstance(parameterType)!]; + return [Activator.CreateInstance(parameterType)]; } } } diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs b/src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs index 359d123801..29f531ade2 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunInfo.cs @@ -1,19 +1,21 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Helpers; namespace BenchmarkDotNet.Running { - public class BenchmarkRunInfo(BenchmarkCase[] benchmarksCase, Type type, ImmutableConfig config, bool containsBenchmarkDeclarations, CompositeInProcessDiagnoser compositeInProcessDiagnoser) : IDisposable + public class BenchmarkRunInfo(BenchmarkCase[] benchmarksCase, Type type, ImmutableConfig config, bool containsBenchmarkDeclarations, CompositeInProcessDiagnoser compositeInProcessDiagnoser) : IDisposable, IAsyncDisposable { public BenchmarkRunInfo(BenchmarkCase[] benchmarksCases, Type type, ImmutableConfig config, CompositeInProcessDiagnoser compositeInProcessDiagnoser) : this(benchmarksCases, type, config, benchmarksCases.Length > 0, compositeInProcessDiagnoser) { } + public ValueTask DisposeAsync() => BenchmarksCases.DisposeAllAsync(); + public void Dispose() { - foreach (var benchmarkCase in BenchmarksCases) - { - benchmarkCase.Dispose(); - } + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + context.ExecuteUntilComplete(DisposeAsync()); } public BenchmarkCase[] BenchmarksCases { get; } = benchmarksCase; diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs index 9a744e6ee1..322634cd8f 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerClean.cs @@ -82,7 +82,7 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu var (supportedBenchmarks, validationErrors) = await GetSupportedBenchmarks(benchmarkRunInfos, resolver).ConfigureAwait(); - validationErrors.AddRange(await Validate(supportedBenchmarks).ConfigureAwait()); + validationErrors.AddRange(await Validate(supportedBenchmarks, cancellationToken).ConfigureAwait()); foreach (var validationError in validationErrors) eventProcessor.OnValidationError(validationError); @@ -122,10 +122,10 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task await foreach (var (buildPartition, buildResult) in BuildSequential(compositeLogger, rootArtifactsFolderPath, sequentialBuildPartitions, globalChronometer, eventProcessor, cancellationToken).ConfigureAwait()) +#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task { buildResults.Add(buildPartition, buildResult); } -#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task } var allBuildsHaveFailed = buildResults.Values.All(buildResult => !buildResult.IsBuildSuccess); @@ -197,10 +197,7 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu // some benchmarks might be using parameters that have locking finalizers // so we need to dispose them after we are done running the benchmarks // see https://github.com/dotnet/BenchmarkDotNet/issues/1383 and https://github.com/dotnet/runtime/issues/314 for more - foreach (var benchmarkInfo in benchmarkRunInfos) - { - benchmarkInfo.Dispose(); - } + await benchmarkRunInfos.DisposeAllAsync().ConfigureAwait(); compositeLogger.WriteLineHeader("// * Artifacts cleanup *"); Cleanup(compositeLogger, new HashSet(artifactsToCleanup.Distinct())); @@ -332,7 +329,7 @@ private static async ValueTask RunCore(BenchmarkRunInfo[] benchmarkRu logFilePath, runEnd.GetTimeSpan() - runStart.GetTimeSpan(), cultureInfo, - [.. await Validate(benchmarkRunInfo).ConfigureAwait(false)], // validate them once again, but don't print the output + [.. await Validate([benchmarkRunInfo], cancellationToken).ConfigureAwait(false)], // validate them once again, but don't print the output [.. config.GetColumnHidingRules()] ), benchmarksToRunCount @@ -402,11 +399,24 @@ private static async ValueTask PrintSummary(ILogger logger, ImmutableConfig conf logger.WriteLineHeader("// ***** BenchmarkRunner: End *****"); } - private static async ValueTask> Validate(params BenchmarkRunInfo[] benchmarks) - => await benchmarks - .ToAsyncEnumerable() - .SelectMany(benchmark => benchmark.Config.GetCompositeValidator().ValidateAsync(new ValidationParameters(benchmark.BenchmarksCases, benchmark.Config))) - .ToArrayAsync().ConfigureAwait(false); + // Written out rather than composed with async LINQ - see CompositeValidator.ValidateAsync for why. + private static async ValueTask> Validate(BenchmarkRunInfo[] benchmarks, CancellationToken cancellationToken) + { + var errors = new List(); + + foreach (var benchmark in benchmarks) + { + var validationParameters = new ValidationParameters(benchmark.BenchmarksCases, benchmark.Config); +#pragma warning disable CA2007 + await foreach (var error in benchmark.Config.GetCompositeValidator().ValidateAsync(validationParameters).ConfigureAwait(cancellationToken)) +#pragma warning restore CA2007 + { + errors.Add(error); + } + } + + return errors; + } private static async ValueTask> BuildInParallel( ILogger logger, @@ -720,26 +730,29 @@ private static void LogTotalTime(ILogger logger, TimeSpan time, int executedBenc continue; } - var validBenchmarks = await benchmarkRunInfo.BenchmarksCases - .ToAsyncEnumerable() - .Where(async (benchmark, _) => + // Written out rather than composed with async LINQ - see CompositeValidator.ValidateAsync for why. + var validBenchmarks = new List(); + foreach (var benchmark in benchmarkRunInfo.BenchmarksCases) + { + var errors = new List(); +#pragma warning disable CA2007 + await foreach (var error in benchmark.GetToolchain().ValidateAsync(benchmark, resolver).ConfigureAwait()) +#pragma warning restore CA2007 { + errors.Add(error); + } - var errors = await benchmark.GetToolchain() - .ValidateAsync(benchmark, resolver) - .ToArrayAsync() - .ConfigureAwait(); - - validationErrors.AddRange(errors); + validationErrors.AddRange(errors); - return !errors.Any(error => error.IsCritical); - }) - .ToArrayAsync() - .ConfigureAwait(); + if (!errors.Any(error => error.IsCritical)) + { + validBenchmarks.Add(benchmark); + } + } runInfos.Add( new BenchmarkRunInfo( - validBenchmarks, + validBenchmarks.ToArray(), benchmarkRunInfo.Type, benchmarkRunInfo.Config, benchmarkRunInfo.CompositeInProcessDiagnoser diff --git a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs index 573930b2fc..0b55f5aaa0 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkRunnerDirty.cs @@ -1,6 +1,7 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Reports; using JetBrains.Annotations; @@ -150,7 +151,7 @@ public static async ValueTask RunAsync(BenchmarkRunInfo[] benchmarkRu private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type type, IConfig? config, string[]? args, CancellationToken cancellationToken) { var summaries = args == null - ? await BenchmarkRunnerClean.Run([BenchmarkConverter.TypeToBenchmarks(type, config)], cancellationToken).ConfigureAwait(false) + ? await BenchmarkRunnerClean.Run([await BenchmarkConverter.TypeToBenchmarksAsync(type, config, cancellationToken).ConfigureAwait()], cancellationToken).ConfigureAwait(false) : await new BenchmarkSwitcher([type]).RunWithDirtyAssemblyResolveHelper(args, config, false, cancellationToken).ConfigureAwait(false); return summaries.SingleOrDefault() @@ -160,7 +161,7 @@ private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type t [MethodImpl(MethodImplOptions.NoInlining)] private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type type, MethodInfo[] methods, IConfig? config, CancellationToken cancellationToken) { - var summaries = await BenchmarkRunnerClean.Run([BenchmarkConverter.MethodsToBenchmarks(type, methods, config)], cancellationToken).ConfigureAwait(false); + var summaries = await BenchmarkRunnerClean.Run([await BenchmarkConverter.MethodsToBenchmarksAsync(type, methods, config, cancellationToken).ConfigureAwait()], cancellationToken).ConfigureAwait(false); return summaries.SingleOrDefault() ?? Summary.ValidationFailed($"No benchmarks found in type '{type.Name}'", string.Empty, string.Empty); @@ -169,15 +170,24 @@ private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type t [MethodImpl(MethodImplOptions.NoInlining)] private static async ValueTask RunWithDirtyAssemblyResolveHelper(Assembly assembly, IConfig? config, string[]? args, CancellationToken cancellationToken) => args == null - ? await BenchmarkRunnerClean.Run(assembly.GetRunnableBenchmarks().Select(type => BenchmarkConverter.TypeToBenchmarks(type, config)).ToArray(), cancellationToken).ConfigureAwait(false) + ? await BenchmarkRunnerClean.Run(await TypesToBenchmarksAsync(assembly.GetRunnableBenchmarks(), config, cancellationToken).ConfigureAwait(), cancellationToken).ConfigureAwait(false) : (await new BenchmarkSwitcher(assembly).RunWithDirtyAssemblyResolveHelper(args, config, false, cancellationToken).ConfigureAwait(false)).ToArray(); [MethodImpl(MethodImplOptions.NoInlining)] private static async ValueTask RunWithDirtyAssemblyResolveHelper(Type[] types, IConfig? config, string[]? args, CancellationToken cancellationToken) => args == null - ? await BenchmarkRunnerClean.Run(types.Select(type => BenchmarkConverter.TypeToBenchmarks(type, config)).ToArray(), cancellationToken).ConfigureAwait(false) + ? await BenchmarkRunnerClean.Run(await TypesToBenchmarksAsync(types, config, cancellationToken).ConfigureAwait(), cancellationToken).ConfigureAwait(false) : (await new BenchmarkSwitcher(types).RunWithDirtyAssemblyResolveHelper(args, config, false, cancellationToken).ConfigureAwait(false)).ToArray(); + // Convert types sequentially (not concurrently) because reading params/arguments sources runs user code. + private static async ValueTask TypesToBenchmarksAsync(IEnumerable types, IConfig? config, CancellationToken cancellationToken) + { + var runInfos = new List(); + foreach (var type in types) + runInfos.Add(await BenchmarkConverter.TypeToBenchmarksAsync(type, config, cancellationToken).ConfigureAwait()); + return runInfos.ToArray(); + } + [MethodImpl(MethodImplOptions.NoInlining)] private static ValueTask RunWithDirtyAssemblyResolveHelper(BenchmarkRunInfo[] benchmarkRunInfos, CancellationToken cancellationToken) => BenchmarkRunnerClean.Run(benchmarkRunInfos, cancellationToken); diff --git a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs index d789b8694a..b169af6d52 100644 --- a/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs +++ b/src/BenchmarkDotNet/Running/BenchmarkSwitcher.cs @@ -4,6 +4,7 @@ using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Parameters; @@ -148,7 +149,7 @@ internal async ValueTask> RunWithDirtyAssemblyResolveHelper if (options.ListBenchmarkCaseMode != ListBenchmarkCaseMode.Disabled) { - PrintList(logger, effectiveConfig, allAvailableTypesWithRunnableBenchmarks, options); + await PrintListAsync(logger, effectiveConfig, allAvailableTypesWithRunnableBenchmarks, options, cancellationToken).ConfigureAwait(); return []; } @@ -161,23 +162,22 @@ internal async ValueTask> RunWithDirtyAssemblyResolveHelper return await ApplesToApples(ImmutableConfigBuilder.Create(effectiveConfig), benchmarksToFilter, logger, options, cancellationToken).ConfigureAwait(false); } - var filteredBenchmarks = TypeFilter.Filter(effectiveConfig, benchmarksToFilter); + var filteredBenchmarks = await TypeFilter.FilterAsync(effectiveConfig, benchmarksToFilter, cancellationToken).ConfigureAwait(); if (filteredBenchmarks.IsEmpty()) { - userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, [.. options.Filters]); + await userInteraction.PrintWrongFilterInfoAsync(benchmarksToFilter, logger, [.. options.Filters], cancellationToken).ConfigureAwait(); return []; } return await BenchmarkRunnerClean.Run(filteredBenchmarks, cancellationToken).ConfigureAwait(false); } - private static void PrintList(ILogger nonNullLogger, IConfig effectiveConfig, IReadOnlyList allAvailableTypesWithRunnableBenchmarks, CommandLineOptions options) + private static async ValueTask PrintListAsync(ILogger nonNullLogger, IConfig effectiveConfig, IReadOnlyList allAvailableTypesWithRunnableBenchmarks, CommandLineOptions options, CancellationToken cancellationToken) { var printer = new BenchmarkCasesPrinter(options.ListBenchmarkCaseMode); - var benchmarkCases = TypeFilter - .Filter(effectiveConfig, allAvailableTypesWithRunnableBenchmarks) + var benchmarkCases = (await TypeFilter.FilterAsync(effectiveConfig, allAvailableTypesWithRunnableBenchmarks, cancellationToken).ConfigureAwait()) .SelectMany(p => p.BenchmarksCases); printer.Print(benchmarkCases, nonNullLogger); @@ -213,10 +213,10 @@ private async ValueTask> ApplesToApples(ImmutableConfig eff invocationCountConfig.RemoveAllDiagnosers(); invocationCountConfig.AddJob(invocationCountJob); - var invocationCountBenchmarks = TypeFilter.Filter(invocationCountConfig, benchmarksToFilter); + var invocationCountBenchmarks = await TypeFilter.FilterAsync(invocationCountConfig, benchmarksToFilter, cancellationToken).ConfigureAwait(); if (invocationCountBenchmarks.IsEmpty()) { - userInteraction.PrintWrongFilterInfo(benchmarksToFilter, logger, [.. options.Filters]); + await userInteraction.PrintWrongFilterInfoAsync(benchmarksToFilter, logger, [.. options.Filters], cancellationToken).ConfigureAwait(); return []; } @@ -230,7 +230,7 @@ private async ValueTask> ApplesToApples(ImmutableConfig eff report => report.AllMeasurements.Single(measurement => measurement.IsWorkload() && measurement.IterationStage == Engines.IterationStage.Actual)); int iterationCount = baselineJob.Run.IterationCount; - BenchmarkRunInfo[] benchmarksWithoutInvocationCount = TypeFilter.Filter(effectiveConfig, benchmarksToFilter); + BenchmarkRunInfo[] benchmarksWithoutInvocationCount = await TypeFilter.FilterAsync(effectiveConfig, benchmarksToFilter, cancellationToken).ConfigureAwait(); BenchmarkRunInfo[] benchmarksWithInvocationCount = benchmarksWithoutInvocationCount .Select(benchmarkInfo => new BenchmarkRunInfo( benchmarkInfo.BenchmarksCases.Select(benchmark => diff --git a/src/BenchmarkDotNet/Running/IUserInteraction.cs b/src/BenchmarkDotNet/Running/IUserInteraction.cs index e697f294e2..0c68d39269 100644 --- a/src/BenchmarkDotNet/Running/IUserInteraction.cs +++ b/src/BenchmarkDotNet/Running/IUserInteraction.cs @@ -6,7 +6,7 @@ internal interface IUserInteraction { void PrintNoBenchmarksError(ILogger logger); - void PrintWrongFilterInfo(IReadOnlyList allTypes, ILogger logger, string[] userFilters); + ValueTask PrintWrongFilterInfoAsync(IReadOnlyList allTypes, ILogger logger, string[] userFilters, CancellationToken cancellationToken); IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger); } diff --git a/src/BenchmarkDotNet/Running/TypeFilter.cs b/src/BenchmarkDotNet/Running/TypeFilter.cs index b9420915b5..25846ee252 100644 --- a/src/BenchmarkDotNet/Running/TypeFilter.cs +++ b/src/BenchmarkDotNet/Running/TypeFilter.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Loggers; @@ -61,9 +62,21 @@ public static (bool allTypesValid, IReadOnlyList runnable) GetTypesWithRun } public static BenchmarkRunInfo[] Filter(IConfig effectiveConfig, IEnumerable types) - => types - .Select(type => BenchmarkConverter.TypeToBenchmarks(type, effectiveConfig)) - .Where(info => info.BenchmarksCases.Any()) - .ToArray(); + { + using var context = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + return context.ExecuteUntilComplete(FilterAsync(effectiveConfig, types, CancellationToken.None)); + } + + public static async ValueTask FilterAsync(IConfig effectiveConfig, IEnumerable types, CancellationToken cancellationToken) + { + var result = new List(); + foreach (var type in types) + { + var info = await BenchmarkConverter.TypeToBenchmarksAsync(type, effectiveConfig, cancellationToken).ConfigureAwait(); + if (info.BenchmarksCases.Any()) + result.Add(info); + } + return result.ToArray(); + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Running/UserInteraction.cs b/src/BenchmarkDotNet/Running/UserInteraction.cs index 86defdc5b4..fa4412e0df 100644 --- a/src/BenchmarkDotNet/Running/UserInteraction.cs +++ b/src/BenchmarkDotNet/Running/UserInteraction.cs @@ -50,9 +50,9 @@ public IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger) return selectedTypes; } - public void PrintWrongFilterInfo(IReadOnlyList allTypes, ILogger logger, string[] userFilters) + public async ValueTask PrintWrongFilterInfoAsync(IReadOnlyList allTypes, ILogger logger, string[] userFilters, CancellationToken cancellationToken) { - var correctionSuggester = new CorrectionsSuggester(allTypes); + var correctionSuggester = await CorrectionsSuggester.CreateAsync(allTypes, cancellationToken).ConfigureAwait(); var filterToNames = userFilters .Select(userFilter => (userFilter: userFilter, suggestedBenchmarkNames: correctionSuggester.SuggestFor(userFilter))) diff --git a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt index 47029d33ac..1c457f8a7b 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkProgram.txt @@ -1,20 +1,13 @@ // // this file must not be importing any namespaces -// we should use full names everywhere to avoid any potential naming conflicts, example: #1007, #778 +// we should use full names everywhere to avoid any potential naming conflicts, example: #778, #1007, #2821 #if !NET7_0_OR_GREATER -// User code could poly-fill types and leave them public, so we disable the warning just in case. +// When the benchmark type declares `required` members the runnable inherits them and the compiler emits +// CompilerFeatureRequiredAttribute on it. That attribute doesn't exist before net7.0, so we poly-fill it. User code +// could poly-fill it too and leave it public, so we disable the conflict warning just in case. #pragma warning disable CS0436 // Type conflicts with imported type -namespace System.Diagnostics.CodeAnalysis -{ - [global::System.AttributeUsage(global::System.AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - internal sealed class SetsRequiredMembersAttribute : global::System.Attribute - { - } -} - namespace System.Runtime.CompilerServices { [global::System.AttributeUsage(global::System.AttributeTargets.All, AllowMultiple = true, Inherited = false)] @@ -31,12 +24,6 @@ namespace System.Runtime.CompilerServices public const global::System.String RefStructs = nameof(RefStructs); public const global::System.String RequiredMembers = nameof(RequiredMembers); } - - [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Struct | global::System.AttributeTargets.Field | global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - internal sealed class RequiredMemberAttribute : global::System.Attribute - { - } } #endif diff --git a/src/BenchmarkDotNet/Templates/BenchmarkType.txt b/src/BenchmarkDotNet/Templates/BenchmarkType.txt index ac9d89c84c..961c40cce3 100644 --- a/src/BenchmarkDotNet/Templates/BenchmarkType.txt +++ b/src/BenchmarkDotNet/Templates/BenchmarkType.txt @@ -2,16 +2,26 @@ [global::BenchmarkDotNet.Attributes.CompilerServices.AggressivelyOptimizeMethods] public sealed class Runnable_$ID$ : $WorkloadTypeName$ { - public static async global::System.Threading.Tasks.ValueTask Run(global::BenchmarkDotNet.Engines.IHost host, global::System.String benchmarkName, global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode) + public static async global::System.Threading.Tasks.ValueTask __Run(global::BenchmarkDotNet.Engines.IHost host, global::System.String benchmarkName, global::BenchmarkDotNet.Diagnosers.RunMode diagnoserRunMode) { - global::BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance = new global::BenchmarkDotNet.Autogenerated.Runnable_$ID$(host.CancellationToken); + global::System.Threading.CancellationToken cancellationToken = host.CancellationToken; + $CancellationTokenAssignment$ + // The constructor captures each instance [ParamsSource]/[ArgumentsSource] enumerable via an out parameter; + // the object initializer then assigns the values (init-/required-capable), while static params and argument fields are assigned afterwards. + // We cannot use [SetsRequiredMembers] and assign them in the ctor, because a source may be async. + global::BenchmarkDotNet.Autogenerated.Runnable_$ID$ instance = new global::BenchmarkDotNet.Autogenerated.Runnable_$ID$($SourceOutArguments$) + { + $CancellationTokenInitializer$ + $ParamsInitializer$ + }; + $StaticParamsAndArgsContent$ host.WriteLine(); foreach (global::System.String infoLine in global::BenchmarkDotNet.Environments.BenchmarkEnvironmentInfo.GetCurrent().ToFormattedString()) { host.WriteLine($"// {infoLine}"); } - global::BenchmarkDotNet.Jobs.Job job = new global::BenchmarkDotNet.Jobs.Job(); // use full name to avoid naming conflicts, #778 + global::BenchmarkDotNet.Jobs.Job job = new global::BenchmarkDotNet.Jobs.Job(); $JobSetDefinition$; job.Freeze(); host.WriteLine($"// Job: {job.DisplayInfo}"); @@ -36,47 +46,11 @@ } await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.BeforeEngine, host.CancellationToken)); - global::BenchmarkDotNet.Engines.EngineParameters engineParameters = new global::BenchmarkDotNet.Engines.EngineParameters() - { - Host = host, - WorkloadMethods = instance.__ResolveWorkloadMethods(host), - WorkloadActionUnroll = instance.WorkloadActionUnroll, - WorkloadActionNoUnroll = instance.WorkloadActionNoUnroll, - OverheadActionNoUnroll = instance.OverheadActionNoUnroll, - OverheadActionUnroll = instance.OverheadActionUnroll, - GlobalSetupAction = instance.__GlobalSetup, - GlobalCleanupAction = instance.__GlobalCleanup, - IterationSetupAction = instance.__IterationSetup, - IterationCleanupAction = instance.__IterationCleanup, - TargetJob = job, - OperationsPerInvoke = $OperationsPerInvoke$, - RunExtraIteration = $RunExtraIteration$, - BenchmarkName = benchmarkName, - InProcessDiagnoserHandler = compositeInProcessDiagnoserHandler - }; - - global::BenchmarkDotNet.Engines.RunResults results = await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(new $EngineFactoryType$().Create(engineParameters).RunAsync()); - host.ReportResults(results); // printing costs memory, do this after runs - - instance.__TrickTheJIT__(); // compile the method for disassembler, but without actual run of the benchmark ;) - await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.AfterEngine, host.CancellationToken).ConfigureAwait(false); - } - - [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers] - public Runnable_$ID$(global::System.Threading.CancellationToken cancellationToken) - { - $InitializeArgumentFields$ - $ParamsContent$ - $CancellationTokenAssignment$ - } - - $DeclareFieldsContainer$ - - private global::System.Reflection.MethodInfo[] __ResolveWorkloadMethods(global::BenchmarkDotNet.Engines.IHost host) - { - // Best-effort: the jit stage uses the resolved method(s) to watch their JIT tier-up events, and falls back - // to a fixed delay when none are resolved. So neither a missed match nor a reflection failure (e.g. a - // same-named overload's parameter type fails to load) may break the benchmark — report and return empty. + // Resolve the benchmark method(s) for the jit stage to watch their JIT tier-up events. Best-effort: it + // falls back to a fixed delay when none are resolved, so neither a missed match nor a reflection failure + // (e.g. a same-named overload's parameter type fails to load) may break the benchmark. Inlined here rather + // than a helper method so it can't collide with a benchmark member's name. + global::System.Reflection.MethodInfo[] workloadMethods = global::System.Array.Empty(); try { global::System.Type[] parameterTypes = $WorkloadMethodParameterTypes$; @@ -104,17 +78,49 @@ } if (isMatch) { - return new global::System.Reflection.MethodInfo[] { candidate }; + workloadMethods = new global::System.Reflection.MethodInfo[] { candidate }; + break; } } + if (workloadMethods.Length == 0) + { + host.WriteLine("// Could not resolve the benchmark method '$WorkloadMethodName$' to watch JIT tier-up events; the jit stage will fall back to a fixed delay."); + } } catch (global::System.Exception e) { - host.SendError($"Exception during __ResolveWorkloadMethods!{(global::System.Environment.NewLine)}{e}"); - return global::System.Array.Empty(); + host.SendError($"Exception during workload method resolution!{(global::System.Environment.NewLine)}{e}"); } - host.WriteLine("// Could not resolve the benchmark method '$WorkloadMethodName$' to watch JIT tier-up events; the jit stage will fall back to a fixed delay."); - return global::System.Array.Empty(); + + global::BenchmarkDotNet.Engines.EngineParameters engineParameters = new global::BenchmarkDotNet.Engines.EngineParameters() + { + Host = host, + WorkloadMethods = workloadMethods, + WorkloadActionUnroll = instance.__WorkloadActionUnroll, + WorkloadActionNoUnroll = instance.__WorkloadActionNoUnroll, + OverheadActionNoUnroll = instance.__OverheadActionNoUnroll, + OverheadActionUnroll = instance.__OverheadActionUnroll, + GlobalSetupAction = instance.__GlobalSetup, + GlobalCleanupAction = instance.__GlobalCleanup, + IterationSetupAction = instance.__IterationSetup, + IterationCleanupAction = instance.__IterationCleanup, + TargetJob = job, + OperationsPerInvoke = $OperationsPerInvoke$, + RunExtraIteration = $RunExtraIteration$, + BenchmarkName = benchmarkName, + InProcessDiagnoserHandler = compositeInProcessDiagnoserHandler + }; + + global::BenchmarkDotNet.Engines.RunResults results = await global::BenchmarkDotNet.Helpers.AwaitHelper.ConfigureAwait(new $EngineFactoryType$().Create(engineParameters).RunAsync()); + host.ReportResults(results); // printing costs memory, do this after runs + + instance.__TrickTheJIT__(); // compile the method for disassembler, but without actual run of the benchmark ;) + await compositeInProcessDiagnoserHandler.HandleAsync(global::BenchmarkDotNet.Engines.BenchmarkSignal.AfterEngine, host.CancellationToken).ConfigureAwait(false); + } + + private Runnable_$ID$($SourceOutParameters$) + { + $SourceCaptures$ } private $GlobalSetupModifiers$ global::System.Threading.Tasks.ValueTask __GlobalSetup() @@ -163,4 +169,6 @@ } $CoreImpl$ + + $DeclareFieldsContainer$ } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Templates/CsProj.txt b/src/BenchmarkDotNet/Templates/CsProj.txt index 9afef8cf5e..b022015923 100644 --- a/src/BenchmarkDotNet/Templates/CsProj.txt +++ b/src/BenchmarkDotNet/Templates/CsProj.txt @@ -31,7 +31,7 @@ - latest + latest $RUNTIMESETTINGS$ diff --git a/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt b/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt index f005cd82b4..313da639dc 100644 --- a/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt +++ b/src/BenchmarkDotNet/Templates/MonoAOTLLVMCsProj.txt @@ -43,7 +43,7 @@ - latest + latest diff --git a/src/BenchmarkDotNet/Templates/R2RCsProj.txt b/src/BenchmarkDotNet/Templates/R2RCsProj.txt index e881418b1b..238ecdb807 100644 --- a/src/BenchmarkDotNet/Templates/R2RCsProj.txt +++ b/src/BenchmarkDotNet/Templates/R2RCsProj.txt @@ -46,7 +46,7 @@ - latest + latest diff --git a/src/BenchmarkDotNet/Templates/WasmCsProj.txt b/src/BenchmarkDotNet/Templates/WasmCsProj.txt index 68915fe4f6..e3e28fd5e7 100644 --- a/src/BenchmarkDotNet/Templates/WasmCsProj.txt +++ b/src/BenchmarkDotNet/Templates/WasmCsProj.txt @@ -45,7 +45,7 @@ $CORECLR_OVERRIDES$ - latest + latest diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs index 13299866d7..4aa179be8b 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitter.cs @@ -6,7 +6,7 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitterBase.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitterBase.cs index 030ea9cf3a..5b216bf10b 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitterBase.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncCoreEmitterBase.cs @@ -7,7 +7,7 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncEnumerableCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncEnumerableCoreEmitter.cs index 1f147e6f15..bb14b59d7a 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncEnumerableCoreEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncEnumerableCoreEmitter.cs @@ -6,7 +6,7 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs index 7646bddf84..cea0b38edc 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/AsyncStateMachineEmitter.cs @@ -3,29 +3,12 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; partial class RunnableEmitter { - // Roslyn generates ordinals in declaration order of every member. - // We don't necessarily emit members in the same order (or at all in the case of Runnable_#.Run), so we map it to the expected Roslyn ordinal. - // This doesn't really matter for the runtime, but it helps with the NaiveRunnableEmitDiff tests. - protected virtual IReadOnlyDictionary AsyncMethodToOrdinalMap { get; } = new Dictionary - { - { GlobalSetupMethodName, 5 }, - { GlobalCleanupMethodName, 6 }, - { IterationSetupMethodName, 7 }, - { IterationCleanupMethodName, 8 }, - { OverheadActionUnrollMethodName, 12 }, - { OverheadActionNoUnrollMethodName, 13 }, - { WorkloadActionUnrollMethodName, 14 }, - { WorkloadActionNoUnrollMethodName, 15 }, - { StartWorkloadMethodName, 16 }, - { WorkloadCoreMethodName, 17 }, - }; - private record struct AsyncStateMachineFields(FieldInfo StateField, FieldInfo BuilderField, FieldInfo? ThisField); private record struct AsyncStateMachineMoveNextInfo(ILGenerator IlBuilder, Label EndTryLabel, Label ReturnLabel, LocalBuilder StateLocal, LocalBuilder? ThisLocal, LocalBuilder? ReturnDefaultLocal); private record struct AsyncStateMachineBuilderInfo(TypeBuilder TypeBuilder, AsyncStateMachineFields PublicFields, AsyncStateMachineMoveNextInfo MoveNextInfo); @@ -49,7 +32,7 @@ instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask __GlobalSetu ) .SetAggressiveOptimizationImplementationFlag(); - // [AsyncStateMachine(typeof(<__GlobalSetup>d__4))] + // [AsyncStateMachine(typeof(<__GlobalSetup>d__2))] var attrCtor = typeof(AsyncStateMachineAttribute).GetConstructor([typeof(Type)]) ?? throw new MissingMemberException(nameof(AsyncStateMachineAttribute)); methodBuilder.SetCustomAttribute(new CustomAttributeBuilder(attrCtor, [asyncStateMachineType])); @@ -57,7 +40,7 @@ instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask __GlobalSetu ILGenerator ilBuilder = methodBuilder.GetILGenerator(); /* .locals init ( - [0] valuetype BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4' + [0] valuetype BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2' ) */ var asyncStateMachineLocal = ilBuilder.DeclareLocal(asyncStateMachineType); @@ -65,7 +48,7 @@ [0] valuetype BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4' // stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create(); IL_0000: ldloca.s 0 IL_0002: call valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create() - IL_0007: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_0007: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>t__builder' */ ilBuilder.EmitLdloca(asyncStateMachineLocal); ilBuilder.Emit(OpCodes.Call, asyncMethodBuilderType.GetMethod(nameof(AsyncTaskMethodBuilder.Create), BindingFlags.Public | BindingFlags.Static)!); @@ -76,7 +59,7 @@ [0] valuetype BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4' // stateMachine.<>4__this = this; IL_000c: ldloca.s 0 IL_000e: ldarg.0 - IL_000f: stfld class BenchmarkDotNet.Autogenerated.Runnable_0 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>4__this' + IL_000f: stfld class BenchmarkDotNet.Autogenerated.Runnable_0 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>4__this' */ ilBuilder.EmitLdloca(asyncStateMachineLocal); ilBuilder.Emit(OpCodes.Ldarg_0); @@ -86,7 +69,7 @@ [0] valuetype BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4' // stateMachine.<>1__state = -1; IL_0014: ldloca.s 0 IL_0016: ldc.i4.m1 - IL_0017: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + IL_0017: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>1__state' */ ilBuilder.EmitLdloca(asyncStateMachineLocal); ilBuilder.Emit(OpCodes.Ldc_I4_M1); @@ -94,9 +77,9 @@ [0] valuetype BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4' /* // stateMachine.<>t__builder.Start(ref stateMachine); IL_001c: ldloca.s 0 - IL_001e: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_001e: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>t__builder' IL_0023: ldloca.s 0 - IL_0025: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Startd__4'>(!!0&) + IL_0025: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Startd__2'>(!!0&) */ ilBuilder.EmitLdloca(asyncStateMachineLocal); ilBuilder.Emit(OpCodes.Ldflda, builderField); @@ -112,7 +95,7 @@ [0] valuetype BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4' /* // return stateMachine.<>t__builder.Task; IL_002a: ldloca.s 0 - IL_002c: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_002c: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>t__builder' IL_0031: call instance class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task() */ ilBuilder.EmitLdloca(asyncStateMachineLocal); @@ -132,9 +115,27 @@ private AsyncStateMachineBuilderInfo BeginAsyncStateMachineTypeBuilder(string ca /* [StructLayout(LayoutKind.Auto)] [CompilerGenerated] - private struct <__GlobalSetup>d__4 : IAsyncStateMachine + private struct <__GlobalSetup>d__2 : IAsyncStateMachine */ - int ordinal = AsyncMethodToOrdinalMap[callerMethodName]; + // Roslyn generates ordinals in declaration order of every member. + // We don't necessarily emit members in the same order (or at all in the case of Runnable_#.Run), so we map it to the expected Roslyn ordinal. + // This doesn't really matter for the runtime, but it helps with the NaiveRunnableEmitDiff tests. + // The fields container is declared last in BenchmarkType.txt precisely so that these are constant: if declared any earlier, + // its struct and field would shift everything after it by two whenever a benchmark has fields at all. + int ordinal = callerMethodName switch + { + GlobalSetupMethodName => 2, + GlobalCleanupMethodName => 3, + IterationSetupMethodName => 4, + IterationCleanupMethodName => 5, + OverheadActionUnrollMethodName => 9, + OverheadActionNoUnrollMethodName => 10, + WorkloadActionUnrollMethodName => 11, + WorkloadActionNoUnrollMethodName => 12, + StartWorkloadMethodName => 13, + WorkloadCoreMethodName => 14, + _ => throw new ArgumentOutOfRangeException(nameof(callerMethodName), callerMethodName, "No Roslyn ordinal is known for this method.") + }; var asyncStateMachineTypeBuilder = runnableBuilder.DefineNestedType( $"<{callerMethodName}>d__{ordinal}", TypeAttributes.NestedPrivate | TypeAttributes.AutoLayout | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, @@ -202,7 +203,7 @@ instance void MoveNext () cil managed flags(0200) /* // int num = <>1__state; IL_0000: ldarg.0 - IL_0001: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + IL_0001: ldfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>1__state' IL_0006: stloc.0 */ ilBuilder.Emit(OpCodes.Ldarg_0); @@ -213,7 +214,7 @@ instance void MoveNext () cil managed flags(0200) /* // Runnable_0 runnable_ = <>4__this; IL_0007: ldarg.0 - IL_0008: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>4__this' + IL_0008: ldfld class BenchmarkDotNet.Autogenerated.Runnable_0 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>4__this' IL_000d: stloc.1 */ ilBuilder.Emit(OpCodes.Ldarg_0); @@ -266,7 +267,7 @@ class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMa /* // <>t__builder.SetStateMachine(stateMachine); IL_0000: ldarg.0 - IL_0001: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_0001: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>t__builder' IL_0006: ldarg.1 IL_0007: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine) IL_000c: ret @@ -298,7 +299,7 @@ private Type CompleteAsyncStateMachineType(Type asyncMethodBuilderType, AsyncSta // <>1__state = -2; IL_006c: ldarg.0 IL_006d: ldc.i4.s -2 - IL_006f: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + IL_006f: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>1__state' */ ilBuilder.Emit(OpCodes.Ldarg_0); ilBuilder.Emit(OpCodes.Ldc_I4_S, (sbyte)-2); @@ -306,7 +307,7 @@ private Type CompleteAsyncStateMachineType(Type asyncMethodBuilderType, AsyncSta /* // <>t__builder.SetException(exception); IL_0074: ldarg.0 - IL_0075: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_0075: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>t__builder' IL_007a: ldloc.3 IL_007b: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::SetException(class [System.Runtime]System.Exception) */ @@ -326,7 +327,7 @@ private Type CompleteAsyncStateMachineType(Type asyncMethodBuilderType, AsyncSta // <>1__state = -2; IL_0082: ldarg.0 IL_0083: ldc.i4.s -2 - IL_0085: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + IL_0085: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>1__state' */ // IL_0082: ilBuilder.MarkLabel(endTryLabel); @@ -337,14 +338,14 @@ private Type CompleteAsyncStateMachineType(Type asyncMethodBuilderType, AsyncSta /* // <>t__builder.SetResult(); IL_008a: ldarg.0 - IL_008b: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_008b: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>t__builder' IL_0090: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::SetResult() -or- // <>t__builder.SetResult(result); IL_018f: ldarg.0 - IL_0190: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1 BenchmarkDotNet.Autogenerated.Runnable_0/'d__17'::'<>t__builder' + IL_0190: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1 BenchmarkDotNet.Autogenerated.Runnable_0/'d__14'::'<>t__builder' IL_0195: ldloc.2 IL_0196: call instance void valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1::SetResult(!0) */ @@ -463,7 +464,7 @@ void EmitMoveNextImpl() IL_0027: ldc.i4.0 IL_0028: dup IL_0029: stloc.0 - IL_002a: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + IL_002a: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>1__state' */ ilBuilder.Emit(OpCodes.Ldarg_0); ilBuilder.Emit(OpCodes.Ldc_I4_0); @@ -474,7 +475,7 @@ void EmitMoveNextImpl() // <>u__1 = awaiter; IL_002f: ldarg.0 IL_0030: ldloc.2 - IL_0031: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>u__1' + IL_0031: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>u__1' */ ilBuilder.Emit(OpCodes.Ldarg_0); ilBuilder.EmitLdloc(awaiterLocal); @@ -482,10 +483,10 @@ void EmitMoveNextImpl() /* // <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); IL_0036: ldarg.0 - IL_0037: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>t__builder' + IL_0037: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>t__builder' IL_003c: ldloca.s 2 IL_003e: ldarg.0 - IL_003f: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::AwaitUnsafeOnCompletedd__4'>(!!0&, !!1&) + IL_003f: call instance void [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder::AwaitUnsafeOnCompletedd__2'>(!!0&, !!1&) */ ilBuilder.Emit(OpCodes.Ldarg_0); ilBuilder.Emit(OpCodes.Ldflda, builderField); @@ -501,7 +502,7 @@ void EmitMoveNextImpl() /* // awaiter = <>u__1; IL_0046: ldarg.0 - IL_0047: ldfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>u__1' + IL_0047: ldfld valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>u__1' IL_004c: stloc.2 */ // IL_0046: @@ -513,7 +514,7 @@ void EmitMoveNextImpl() /* // <>u__1 = default(TaskAwaiter); IL_004d: ldarg.0 - IL_004e: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>u__1' + IL_004e: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>u__1' IL_0053: initobj [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter */ ilBuilder.Emit(OpCodes.Ldarg_0); @@ -524,7 +525,7 @@ void EmitMoveNextImpl() IL_005a: ldc.i4.m1 IL_005b: dup IL_005c: stloc.0 - IL_005d: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__4'::'<>1__state' + IL_005d: stfld int32 BenchmarkDotNet.Autogenerated.Runnable_0/'<__GlobalSetup>d__2'::'<>1__state' */ ilBuilder.Emit(OpCodes.Ldarg_0); ilBuilder.Emit(OpCodes.Ldc_I4_M1); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs index 7267d909cb..31a0fb74bd 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/RunnableEmitter.cs @@ -13,7 +13,7 @@ using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Security; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation @@ -271,12 +271,12 @@ private void EmitFields() if (parameters.Length + GetExtraFieldsCount() > 0) { /* - private unsafe struct FieldsContainer + private unsafe struct __FieldsContainer { } */ var fieldsContainerBuilder = runnableBuilder.DefineNestedType( - "FieldsContainer", + FieldsContainerTypeName, TypeAttributes.NestedPrivate | TypeAttributes.AutoLayout | TypeAttributes.Sealed | TypeAttributes.BeforeFieldInit, typeof(ValueType)); nestedTypeBuilders.Add(fieldsContainerBuilder); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncCoreEmitter.cs index e4172f58a4..24c8932b6d 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncCoreEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncCoreEmitter.cs @@ -3,7 +3,7 @@ using Perfolizer.Horology; using System.Reflection; using System.Reflection.Emit; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs index 1e4350f3d6..43513c5da1 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Emitters/SyncTaskCoreEmitter.cs @@ -5,7 +5,7 @@ using Perfolizer.Horology; using System.Reflection; using System.Reflection.Emit; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation; @@ -16,21 +16,6 @@ partial class RunnableEmitter // so the iteration loop stays synchronous, matching the pre-async-refactor behavior so historical results stay comparable. private sealed class SyncTaskCoreEmitter(BuildPartition buildPartition, ModuleBuilder moduleBuilder, BenchmarkBuildInfo benchmark) : RunnableEmitter(buildPartition, moduleBuilder, benchmark) { - // The workload is consumed synchronously, so the only async state machines are the setup/cleanup - // methods. Without arguments there is no fields container declared before them, so their Roslyn - // ordinals are two lower than in the async path (which always declares the fields container). - // With arguments the fields container shifts them back to the async ordinals. - protected override IReadOnlyDictionary AsyncMethodToOrdinalMap - => argFields.Count > 0 - ? base.AsyncMethodToOrdinalMap - : new Dictionary - { - { GlobalSetupMethodName, 3 }, - { GlobalCleanupMethodName, 4 }, - { IterationSetupMethodName, 5 }, - { IterationCleanupMethodName, 6 }, - }; - protected override void EmitExtraGlobalCleanup(ILGenerator ilBuilder, LocalBuilder? thisLocal) { } protected override void EmitCoreImpl() diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs deleted file mode 100644 index be04e0ce5e..0000000000 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableConstants.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation -{ - internal class RunnableConstants - { - public const string OpImplicitMethodName = "op_Implicit"; - - public const string DynamicAssemblySuffix = "Emitted"; - public const string EmittedTypePrefix = "BenchmarkDotNet.Autogenerated.Runnable_"; - public const string ArgFieldPrefix = "argField"; - public const string ArgParamPrefix = "arg"; - public const string FieldsContainerName = "__fieldsContainer"; - - public const string TrickTheJitCoreMethodName = "__TrickTheJIT__"; - public const string OverheadImplementationMethodName = "__Overhead"; - public const string OverheadActionUnrollMethodName = "OverheadActionUnroll"; - public const string OverheadActionNoUnrollMethodName = "OverheadActionNoUnroll"; - public const string WorkloadActionUnrollMethodName = "WorkloadActionUnroll"; - public const string WorkloadActionNoUnrollMethodName = "WorkloadActionNoUnroll"; - public const string ForDisassemblyDiagnoserMethodName = "__ForDisassemblyDiagnoser__"; - public const string InvokeCountParamName = "invokeCount"; - public const string ClockParamName = "clock"; - - public const string GlobalSetupMethodName = "__GlobalSetup"; - public const string GlobalCleanupMethodName = "__GlobalCleanup"; - public const string IterationSetupMethodName = "__IterationSetup"; - public const string IterationCleanupMethodName = "__IterationCleanup"; - - public const string WorkloadValueTaskSourceFieldName = "workloadValueTaskSource"; - public const string ClockFieldName = "clock"; - public const string InvokeCountFieldName = "invokeCount"; - public const string StartWorkloadMethodName = "__StartWorkload"; - public const string WorkloadCoreMethodName = "__WorkloadCore"; - } -} \ No newline at end of file diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs index bc0e4fdfb9..bdba15ba27 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/Implementation/Runnable/RunnableReflectionHelpers.cs @@ -1,8 +1,9 @@ +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Parameters; using BenchmarkDotNet.Running; using Perfolizer.Horology; using System.Reflection; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; namespace BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation { @@ -46,7 +47,7 @@ internal static class RunnableReflectionHelpers { return owner.GetMethods(BindingFlagsPublicStatic) .FirstOrDefault(m => - m.Name == OpImplicitMethodName + m.Name == ReflectionExtensions.OpImplicitMethodName && m.ReturnType == to && m.GetParameters().Single().ParameterType == from); } @@ -76,17 +77,18 @@ public static void SetParameter(object instance, ParameterInstance paramInfo) var bindingFlags = paramInfo.IsStatic ? BindingFlagsAllStatic : BindingFlagsAllInstance; var type = instance.GetType(); - if (type.GetProperty(paramInfo.Name, bindingFlags) is var p && p != null) + switch (type.GetParameterMember(paramInfo.Name, paramInfo.Definition.ParameterType, bindingFlags)) { - p.SetValue(instanceArg, TryChangeType(paramInfo.Value, p.PropertyType)); - } - else if (type.GetField(paramInfo.Name, bindingFlags) is var f && f != null) - { - f.SetValue(instanceArg, TryChangeType(paramInfo.Value, f.FieldType)); - } - else - { - throw new InvalidOperationException($"Can't find a member {paramInfo.ToDisplayText()}."); + case PropertyInfo p: + p.SetValue(instanceArg, TryChangeType(paramInfo.Value, p.PropertyType)); + break; + + case FieldInfo f: + f.SetValue(instanceArg, TryChangeType(paramInfo.Value, f.FieldType)); + break; + + default: + throw new InvalidOperationException($"Can't find a member {paramInfo.ToDisplayText()}."); } } diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs index cc7ba1b5e4..1ac0506150 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitGenerator.cs @@ -32,7 +32,7 @@ public async ValueTask GenerateProjectAsync(BuildPartition build private ArtifactsPaths GetArtifactsPaths(BuildPartition buildPartition, string rootArtifactsFolderPath) { - string programName = buildPartition.ProgramName + RunnableConstants.DynamicAssemblySuffix; + string programName = $"{buildPartition.ProgramName}Emitted"; string buildArtifactsDirectoryPath = GetBuildArtifactsDirectoryPath(buildPartition); string binariesDirectoryPath = GetBinariesDirectoryPath(buildArtifactsDirectoryPath); diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs index 9b01753346..df66a332f1 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/Emit/InProcessEmitRunner.cs @@ -6,7 +6,7 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains.Parameters; using BenchmarkDotNet.Validators; -using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableConstants; +using static BenchmarkDotNet.Code.RunnableConstants; using static BenchmarkDotNet.Toolchains.InProcess.Emit.Implementation.RunnableReflectionHelpers; namespace BenchmarkDotNet.Toolchains.InProcess.Emit; @@ -141,7 +141,7 @@ private static void FillMembers(object instance, BenchmarkCase benchmarkCase, Sy // Inject CancellationToken into properties/fields marked with [BenchmarkCancellation] var targetType = benchmarkCase.Descriptor.Type; - foreach (var property in targetType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static)) + foreach (var property in targetType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.FlattenHierarchy)) { if (property.PropertyType == typeof(System.Threading.CancellationToken) && property.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false)) @@ -155,7 +155,7 @@ private static void FillMembers(object instance, BenchmarkCase benchmarkCase, Sy } } - foreach (var field in targetType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static)) + foreach (var field in targetType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.FlattenHierarchy)) { if (field.FieldType == typeof(System.Threading.CancellationToken) && field.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false)) diff --git a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs index 772416984f..326f858e9f 100644 --- a/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs +++ b/src/BenchmarkDotNet/Toolchains/InProcess/NoEmit/InProcessNoEmitRunner.cs @@ -1,6 +1,7 @@ using BenchmarkDotNet.Engines; using BenchmarkDotNet.Environments; using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; @@ -75,35 +76,35 @@ internal static void FillMembers(object instance, BenchmarkCase benchmarkCase, C // Fill parameter values foreach (var parameter in benchmarkCase.Parameters.Items) { - var flags = BindingFlags.Public; - flags |= parameter.IsStatic ? BindingFlags.Static : BindingFlags.Instance; + if (parameter.IsArgument) + continue; - var paramProperty = targetType.GetProperty(parameter.Name, flags); + var flags = BindingFlags.Public | BindingFlags.FlattenHierarchy + | (parameter.IsStatic ? BindingFlags.Static : BindingFlags.Instance); - if (paramProperty == null) + switch (targetType.GetParameterMember(parameter.Name, parameter.Definition.ParameterType, flags)) { - var paramField = targetType.GetField(parameter.Name, flags); - if (paramField == null) - throw new InvalidOperationException( - $"Type {targetType.FullName}: no property or field {parameter.Name} found."); + case FieldInfo paramField: + paramField.SetValue(paramField.IsStatic ? null : instance, parameter.Value); + break; - var callInstance = paramField.IsStatic ? null : instance; - paramField.SetValue(callInstance, parameter.Value); - } - else - { - var setter = paramProperty.GetSetMethod(); - if (setter == null) - throw new InvalidOperationException( - $"Type {targetType.FullName}: no settable property {parameter.Name} found."); + case PropertyInfo paramProperty: + var setter = paramProperty.GetSetMethod(); + if (setter == null) + throw new InvalidOperationException( + $"Type {targetType.FullName}: no settable property {parameter.Name} found."); + + setter.Invoke(setter.IsStatic ? null : instance, [parameter.Value]); + break; - var callInstance = setter.IsStatic ? null : instance; - setter.Invoke(callInstance, [parameter.Value]); + default: + throw new InvalidOperationException( + $"Type {targetType.FullName}: no property or field {parameter.Name} found."); } } // Inject CancellationToken into properties/fields marked with [BenchmarkCancellation] - foreach (var property in targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) + foreach (var property in targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy)) { if (property.PropertyType == typeof(CancellationToken) && property.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false)) @@ -117,7 +118,7 @@ internal static void FillMembers(object instance, BenchmarkCase benchmarkCase, C } } - foreach (var field in targetType.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)) + foreach (var field in targetType.GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy)) { if (field.FieldType == typeof(CancellationToken) && field.IsDefined(typeof(Attributes.BenchmarkCancellationAttribute), inherit: false)) diff --git a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs index 7ce4b32a5e..85ef57e7f2 100644 --- a/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs +++ b/src/BenchmarkDotNet/Toolchains/NativeAot/Generator.cs @@ -175,7 +175,7 @@ private string GenerateProjectForNuGetBuild(string projectFilePath, BuildPartiti {GetCustomProperties(buildPartition, logger)} - latest + latest """; diff --git a/src/BenchmarkDotNet/Toolchains/Roslyn/Builder.cs b/src/BenchmarkDotNet/Toolchains/Roslyn/Builder.cs index 496e2add38..8886aa07a9 100644 --- a/src/BenchmarkDotNet/Toolchains/Roslyn/Builder.cs +++ b/src/BenchmarkDotNet/Toolchains/Roslyn/Builder.cs @@ -45,7 +45,7 @@ private async ValueTask Build(GenerateResult generateResult, BuildP var syntaxTree = CSharpSyntaxTree.ParseText( text: await File.ReadAllTextAsync(generateResult.ArtifactsPaths.ProgramCodePath, cancellationToken).ConfigureAwait(false), // this version is used to parse the boilerplate code generated by BDN, so that benchmark themselves can use more recent version - options: new CSharpParseOptions(LanguageVersion.CSharp8), + options: new CSharpParseOptions(LanguageVersion.CSharp9), cancellationToken: cancellationToken); var compilationOptions = new CSharpCompilationOptions( diff --git a/src/BenchmarkDotNet/Validators/BenchmarkCancellationValidator.cs b/src/BenchmarkDotNet/Validators/BenchmarkCancellationValidator.cs index 785881f813..783142e1e6 100644 --- a/src/BenchmarkDotNet/Validators/BenchmarkCancellationValidator.cs +++ b/src/BenchmarkDotNet/Validators/BenchmarkCancellationValidator.cs @@ -13,13 +13,12 @@ public class BenchmarkCancellationValidator : IValidator public IAsyncEnumerable ValidateAsync(ValidationParameters input) => input.Benchmarks .Select(benchmark => benchmark.Descriptor.Type) .Distinct() - .ToAsyncEnumerable() - .SelectMany(ValidateAsync); + .SelectMany(ValidateAsync) + .ToAsyncEnumerable(); - private async IAsyncEnumerable ValidateAsync(Type type) + private IEnumerable ValidateAsync(Type type) { - const BindingFlags reflectionFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | - BindingFlags.FlattenHierarchy; + const BindingFlags reflectionFlags = ReflectionExtensions.ParameterMemberFlags; foreach (var memberInfo in type.GetMembers(reflectionFlags)) { var attribute = memberInfo.ResolveAttribute(); diff --git a/src/BenchmarkDotNet/Validators/CompositeValidator.cs b/src/BenchmarkDotNet/Validators/CompositeValidator.cs index 3d9e03f813..558f668ff3 100644 --- a/src/BenchmarkDotNet/Validators/CompositeValidator.cs +++ b/src/BenchmarkDotNet/Validators/CompositeValidator.cs @@ -1,4 +1,6 @@ +using BenchmarkDotNet.Helpers; using System.Collections.Immutable; +using System.Runtime.CompilerServices; namespace BenchmarkDotNet.Validators { @@ -14,7 +16,31 @@ internal class CompositeValidator : IValidator public bool TreatsWarningsAsErrors => validators.Any(validator => validator.TreatsWarningsAsErrors); + // Written out rather than composed with async LINQ: a validator that awaits user code suspends, and an + // operator driving it would resume on whatever SynchronizationContext is ambient around the run. BenchmarkDotNet + // installs none of its own, so that is the caller's - which may be single-threaded and, while the pump blocks + // its thread, unable to run the continuation at all. public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) - => validators.ToAsyncEnumerable().SelectMany(validator => validator.ValidateAsync(validationParameters)).Distinct(); + => ValidateAsyncCore(validationParameters); + + // The token reaches this through the consumer's ConfigureAwait/WithCancellation on the returned enumerable, + // and is forwarded so it reaches each validator's own enumerator (the ExecutionValidatorBase pattern). + private async IAsyncEnumerable ValidateAsyncCore(ValidationParameters validationParameters, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var reported = new HashSet(); + + foreach (var validator in validators) + { +#pragma warning disable CA2007 + await foreach (var error in validator.ValidateAsync(validationParameters).ConfigureAwait(cancellationToken)) +#pragma warning restore CA2007 + { + if (reported.Add(error)) + { + yield return error; + } + } + } + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/DiagnosersValidator.cs b/src/BenchmarkDotNet/Validators/DiagnosersValidator.cs index 933c197a8f..50c8b09bcc 100644 --- a/src/BenchmarkDotNet/Validators/DiagnosersValidator.cs +++ b/src/BenchmarkDotNet/Validators/DiagnosersValidator.cs @@ -1,3 +1,6 @@ +using BenchmarkDotNet.Helpers; +using System.Runtime.CompilerServices; + namespace BenchmarkDotNet.Validators { public class DiagnosersValidator : IValidator @@ -10,11 +13,22 @@ private DiagnosersValidator() public bool TreatsWarningsAsErrors => true; + // Written out rather than composed with async LINQ - see CompositeValidator.ValidateAsync for why. The + // diagnosers here are third-party implementations, so their sequences can suspend on anything. public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) - => validationParameters - .Config - .GetDiagnosers() - .ToAsyncEnumerable() - .SelectMany(diagnoser => diagnoser.ValidateAsync(validationParameters)); + => ValidateAsyncCore(validationParameters); + + private static async IAsyncEnumerable ValidateAsyncCore(ValidationParameters validationParameters, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var diagnoser in validationParameters.Config.GetDiagnosers()) + { +#pragma warning disable CA2007 + await foreach (var error in diagnoser.ValidateAsync(validationParameters).ConfigureAwait(cancellationToken)) +#pragma warning restore CA2007 + { + yield return error; + } + } + } } } \ No newline at end of file diff --git a/src/BenchmarkDotNet/Validators/ExecutionValidator.cs b/src/BenchmarkDotNet/Validators/ExecutionValidator.cs index 2ec6c19e6f..a05d6006f5 100644 --- a/src/BenchmarkDotNet/Validators/ExecutionValidator.cs +++ b/src/BenchmarkDotNet/Validators/ExecutionValidator.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Helpers; using BenchmarkDotNet.Running; @@ -28,14 +29,14 @@ protected override async IAsyncEnumerable ValidateAsyncCore(Val { continue; } - if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalSetupMethod, errors, cancellationToken).ConfigureAwait()) + if (await TryToCallSetupOrCleanup(benchmark, benchmarkTypeInstance, benchmark.Descriptor.GlobalSetupMethod, errors, cancellationToken).ConfigureAwait()) { - if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationSetupMethod, errors, cancellationToken).ConfigureAwait()) + if (await TryToCallSetupOrCleanup(benchmark, benchmarkTypeInstance, benchmark.Descriptor.IterationSetupMethod, errors, cancellationToken).ConfigureAwait()) { await ExecuteBenchmarkAsync(benchmarkTypeInstance, benchmark, args, errors, cancellationToken).ConfigureAwait(); - await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationCleanupMethod, errors, cancellationToken).ConfigureAwait(); + await TryToCallSetupOrCleanup(benchmark, benchmarkTypeInstance, benchmark.Descriptor.IterationCleanupMethod, errors, cancellationToken).ConfigureAwait(); } - await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalCleanupMethod, errors, cancellationToken).ConfigureAwait(); + await TryToCallSetupOrCleanup(benchmark, benchmarkTypeInstance, benchmark.Descriptor.GlobalCleanupMethod, errors, cancellationToken).ConfigureAwait(); } } @@ -50,9 +51,14 @@ private async ValueTask ExecuteBenchmarkAsync(object benchmarkTypeInstance, Benc try { var workloadMethod = benchmark.Descriptor.WorkloadMethod; - var result = workloadMethod.Invoke(benchmarkTypeInstance, args); + if (workloadMethod.ReturnType.WithoutRefModifier().IsByRefLike()) + { + errors.Add(new ValidationError(false, $"Benchmark '{benchmark.DisplayInfo}' returns by-ref-like value, skipping execution validation.", benchmark)); + return; + } if (workloadMethod.ReturnType.IsAwaitable(out var awaitableInfo)) { + var result = workloadMethod.Invoke(benchmarkTypeInstance, args); if (result is null) { errors.Add(new ValidationError(TreatsWarningsAsErrors, $"Awaitable benchmark '{benchmark.DisplayInfo}' returned null", benchmark)); @@ -62,12 +68,27 @@ private async ValueTask ExecuteBenchmarkAsync(object benchmarkTypeInstance, Benc } else if (workloadMethod.ReturnType.IsAsyncEnumerable(out var asyncEnumerableInfo)) { + if (asyncEnumerableInfo.CurrentProperty.PropertyType.IsByRefLike()) + { + errors.Add(new ValidationError(false, $"Async enumerable benchmark '{benchmark.DisplayInfo}' yields by-ref-like elements, skipping execution validation.", benchmark)); + return; + } + var result = workloadMethod.Invoke(benchmarkTypeInstance, args); if (result is null) { errors.Add(new ValidationError(TreatsWarningsAsErrors, $"Async enumerable benchmark '{benchmark.DisplayInfo}' returned null", benchmark)); return; } - await DynamicAwaitHelper.DrainAsyncEnumerableAsync(result, asyncEnumerableInfo).ConfigureAwait(false); + // Mirrors real benchmark execution. + await foreach (var item in DynamicAwaitHelper.EnumerateBenchmarkAsync(result, asyncEnumerableInfo).ConfigureAwait(false)) + { + DeadCodeEliminationHelper.KeepAliveWithoutBoxing(item); + } + } + else + { + var result = workloadMethod.Invoke(benchmarkTypeInstance, args); + DeadCodeEliminationHelper.KeepAliveWithoutBoxing(result); } } catch (Exception ex) when (!ExceptionHelper.IsProperCancelation(ex, cancellationToken)) diff --git a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs index bb1eb888c6..646123ed58 100644 --- a/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs +++ b/src/BenchmarkDotNet/Validators/ExecutionValidatorBase.cs @@ -39,7 +39,7 @@ protected bool TryCreateBenchmarkTypeInstance(Type type, List e } } - protected async ValueTask TryToCallSetupOrCleanup(object benchmarkTypeInstance, MethodInfo? method, List errors, CancellationToken cancellationToken) + protected async ValueTask TryToCallSetupOrCleanup(BenchmarkCase benchmark, object benchmarkTypeInstance, MethodInfo? method, List errors, CancellationToken cancellationToken) { if (method is null) { @@ -53,7 +53,7 @@ protected async ValueTask TryToCallSetupOrCleanup(object benchmarkTypeI { if (result is null) { - errors.Add(new ValidationError(TreatsWarningsAsErrors, $"[{GetAttributeName(typeof(T))}] for {benchmarkTypeInstance.GetType().Name} returned null")); + errors.Add(new ValidationError(TreatsWarningsAsErrors, $"[{GetAttributeName(typeof(T))}] for '{benchmark.DisplayInfo}' returned null", benchmark)); return false; } await DynamicAwaitHelper.AwaitResult(result, awaitableInfo).ConfigureAwait(false); @@ -61,9 +61,8 @@ protected async ValueTask TryToCallSetupOrCleanup(object benchmarkTypeI } catch (Exception ex) when (!ExceptionHelper.IsProperCancelation(ex, cancellationToken)) { - errors.Add(new ValidationError( - TreatsWarningsAsErrors, - $"Failed to execute [{GetAttributeName(typeof(T))}] for {benchmarkTypeInstance.GetType().Name}, exception was {GetDisplayExceptionMessage(ex)}")); + errors.Add(new ValidationError(TreatsWarningsAsErrors, + $"Failed to execute [{GetAttributeName(typeof(T))}] for '{benchmark.DisplayInfo}', exception was {GetDisplayExceptionMessage(ex)}", benchmark)); return false; } @@ -86,11 +85,9 @@ protected bool TryFillParamsAndGetArgs(BenchmarkCase benchmark, object benchmark { if (!param.IsArgument) continue; - if (param.Definition.ParameterType.IsByRefLike()) + if (param.Definition.ParameterType.WithoutRefModifier().IsByRefLike()) { - errors.Add(new ValidationError( - TreatsWarningsAsErrors, - $"{GetType().Name} cannot execute benchmark with ref struct parameter {benchmark.Descriptor.Type.Name}.{benchmark.Descriptor.WorkloadMethodDisplayInfo}")); + errors.Add(new ValidationError(false, $"Benchmark '{benchmark.DisplayInfo}' contains a by-ref-like parameter, skipping validation.", benchmark)); args = null; return false; } @@ -102,9 +99,7 @@ protected bool TryFillParamsAndGetArgs(BenchmarkCase benchmark, object benchmark } catch (Exception ex) when (!ExceptionHelper.IsProperCancelation(ex, cancellationToken)) { - errors.Add(new ValidationError( - TreatsWarningsAsErrors, - $"Failed to set parameters for {benchmark.Descriptor.Type.Name}, exception was: {GetDisplayExceptionMessage(ex)}")); + errors.Add(new ValidationError(TreatsWarningsAsErrors, $"Failed to set parameters for {benchmark.Descriptor.Type.Name}, exception was: {GetDisplayExceptionMessage(ex)}", benchmark)); args = null; return false; } diff --git a/src/BenchmarkDotNet/Validators/ParamsAllValuesValidator.cs b/src/BenchmarkDotNet/Validators/ParamsAllValuesValidator.cs index 0086eb2304..1f9740289d 100644 --- a/src/BenchmarkDotNet/Validators/ParamsAllValuesValidator.cs +++ b/src/BenchmarkDotNet/Validators/ParamsAllValuesValidator.cs @@ -13,13 +13,11 @@ public class ParamsAllValuesValidator : IValidator private ParamsAllValuesValidator() { } - private const BindingFlags ReflectionFlags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - public IAsyncEnumerable ValidateAsync(ValidationParameters input) => input.Benchmarks .Select(benchmark => benchmark.Descriptor.Type) .Distinct() - .SelectMany(type => type.GetTypeMembersWithGivenAttribute(ReflectionFlags)) + .SelectMany(type => type.GetTypeMembersWithGivenAttribute(ReflectionExtensions.ParameterMemberFlags)) .Distinct() .Select(member => GetErrorOrDefault(member.ParameterType)) .WhereNotNull() diff --git a/src/BenchmarkDotNet/Validators/ParamsValidator.cs b/src/BenchmarkDotNet/Validators/ParamsValidator.cs index 7be74563b3..9bd8a233d8 100644 --- a/src/BenchmarkDotNet/Validators/ParamsValidator.cs +++ b/src/BenchmarkDotNet/Validators/ParamsValidator.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Code; using BenchmarkDotNet.Extensions; using System.Reflection; @@ -13,14 +14,19 @@ public class ParamsValidator : IValidator public IAsyncEnumerable ValidateAsync(ValidationParameters input) => input.Benchmarks .Select(benchmark => benchmark.Descriptor.Type) .Distinct() - .ToAsyncEnumerable() - .SelectMany(ValidateAsync); + .SelectMany(ValidateAsync) + .ToAsyncEnumerable(); - private async IAsyncEnumerable ValidateAsync(Type type) + private static bool IsStatic(MemberInfo member) => member switch { - const BindingFlags reflectionFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | - BindingFlags.FlattenHierarchy; - foreach (var memberInfo in type.GetMembers(reflectionFlags)) + FieldInfo field => field.IsStatic, + PropertyInfo property => (property.GetMethod ?? property.SetMethod)!.IsStatic, + _ => false + }; + + private IEnumerable ValidateAsync(Type type) + { + foreach (var memberInfo in type.GetMembers(ReflectionExtensions.ParameterMemberFlags)) { var attributes = new Attribute?[] { @@ -36,6 +42,14 @@ private async IAsyncEnumerable ValidateAsync(Type type) string name = $"{type.Name}.{memberInfo.Name}"; string attributeString = string.Join(", ", attributes.Select(attribute => $"[{attribute.GetType().Name.Replace(nameof(Attribute), "")}]")); + // The runnable derives from the benchmark type and assigns each instance parameter member through an + // object initializer, which binds the member name unqualified. A parameter member named like a + // generated member (all __-prefixed) binds to the generated member instead and fails to compile. + // Static members are assigned fully type-qualified, so they cannot collide. + if (!IsStatic(memberInfo) && RunnableConstants.ReservedInstanceMemberNames.Contains(memberInfo.Name)) + yield return new ValidationError(TreatsWarningsAsErrors, + $"Unable to use {name} with {attributeString} because '{memberInfo.Name}' is a reserved name used by BenchmarkDotNet's code generation. Please, rename the member."); + if (attributes.Count > 1) yield return new ValidationError(TreatsWarningsAsErrors, $"Unable to use {name} with {attributeString} at the same time. Please, use a single attribute."); @@ -56,10 +70,7 @@ private async IAsyncEnumerable ValidateAsync(Type type) if (memberInfo is PropertyInfo propertyInfo) { - if (propertyInfo.IsInitOnly()) - yield return new ValidationError(TreatsWarningsAsErrors, - $"Unable to use {name} with {attributeString} because it's init-only. Please, provide a public setter."); - + // An init-only setter is fine: the runnable assigns parameters through an object initializer. if (propertyInfo.SetMethod == null) yield return new ValidationError(TreatsWarningsAsErrors, $"Unable to use {name} with {attributeString} because it has no setter. Please, provide a public setter."); diff --git a/src/BenchmarkDotNet/Validators/RequiredMemberValidator.cs b/src/BenchmarkDotNet/Validators/RequiredMemberValidator.cs new file mode 100644 index 0000000000..060d069e32 --- /dev/null +++ b/src/BenchmarkDotNet/Validators/RequiredMemberValidator.cs @@ -0,0 +1,62 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Extensions; +using System.Reflection; + +namespace BenchmarkDotNet.Validators; + +/// +/// Validates that every `required` member of a benchmark type is one BenchmarkDotNet can set when it +/// constructs the type. The runtime counterpart of the BDN1109 and BDN1110 analyzer rules. +/// +public class RequiredMemberValidator : IValidator +{ + public static readonly RequiredMemberValidator FailOnError = new(); + + // Emitted by the compiler; referenced by name so this also works on target frameworks without the types. + private const string RequiredMemberAttributeFullName = "System.Runtime.CompilerServices.RequiredMemberAttribute"; + private const string SetsRequiredMembersAttributeFullName = "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute"; + + public bool TreatsWarningsAsErrors => true; + + public IAsyncEnumerable ValidateAsync(ValidationParameters input) => input.Benchmarks + .Select(benchmark => benchmark.Descriptor.Type) + .Distinct() + .SelectMany(ValidateAsync) + .ToAsyncEnumerable(); + + private IEnumerable ValidateAsync(Type type) + { + const BindingFlags reflectionFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + // The generated runnable derives from the benchmark type, so its constructor chains to this one. C# would + // force that constructor to repeat [SetsRequiredMembers] (CS9039), which suppresses required-member + // checking entirely and would silently hide required members BenchmarkDotNet cannot set. + var constructor = type.GetConstructor(reflectionFlags, binder: null, Type.EmptyTypes, modifiers: null); + if (constructor != null && HasAttribute(constructor, SetsRequiredMembersAttributeFullName)) + yield return new ValidationError(TreatsWarningsAsErrors, + $"Unable to use {type.Name} because its constructor is annotated with [SetsRequiredMembers], which BenchmarkDotNet's generated constructor cannot honor. Please, remove the attribute and set each required member with [Params], [ParamsSource], [ParamsAllValues] or [BenchmarkCancellation]."); + + // Only fields and properties can be `required`. Note the attribute is also stamped on any *type* that + // declares required members, so a nested type would look required if we asked for all members. + var members = type.GetFields(reflectionFlags).Cast().Concat(type.GetProperties(reflectionFlags)); + + foreach (var memberInfo in members) + { + if (!HasAttribute(memberInfo, RequiredMemberAttributeFullName) || IsSetByBenchmarkDotNet(memberInfo)) + continue; + + yield return new ValidationError(TreatsWarningsAsErrors, + $"Unable to use {type.Name}.{memberInfo.Name} because it's a required member that BenchmarkDotNet cannot set. Please, remove the 'required' modifier or annotate it with [Params], [ParamsSource], [ParamsAllValues] or [BenchmarkCancellation]."); + } + } + + // BenchmarkDotNet assigns these when it constructs the benchmark, so they satisfy the `required` modifier. + private static bool IsSetByBenchmarkDotNet(MemberInfo member) + => member.ResolveAttribute() != null + || member.ResolveAttribute() != null + || member.ResolveAttribute() != null + || member.ResolveAttribute() != null; + + private static bool HasAttribute(MemberInfo member, string attributeFullName) + => member.GetCustomAttributesData().Any(attribute => attribute.AttributeType.FullName == attributeFullName); +} diff --git a/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs b/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs index 6d54901dbd..0c912d18c3 100644 --- a/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs +++ b/src/BenchmarkDotNet/Validators/ReturnValueValidator.cs @@ -35,18 +35,18 @@ protected override async IAsyncEnumerable ValidateAsyncCore(Val { continue; } - if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalSetupMethod, errors, cancellationToken).ConfigureAwait()) + if (await TryToCallSetupOrCleanup(benchmark, benchmarkTypeInstance, benchmark.Descriptor.GlobalSetupMethod, errors, cancellationToken).ConfigureAwait()) { - if (await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationSetupMethod, errors, cancellationToken).ConfigureAwait()) + if (await TryToCallSetupOrCleanup(benchmark, benchmarkTypeInstance, benchmark.Descriptor.IterationSetupMethod, errors, cancellationToken).ConfigureAwait()) { var (hasResult, result) = await ExecuteBenchmarkAsync(benchmarkTypeInstance, benchmark, args, errors, cancellationToken).ConfigureAwait(); if (hasResult) { results.Add((benchmark, result)); } - await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.IterationCleanupMethod, errors, cancellationToken).ConfigureAwait(); + await TryToCallSetupOrCleanup(benchmark, benchmarkTypeInstance, benchmark.Descriptor.IterationCleanupMethod, errors, cancellationToken).ConfigureAwait(); } - await TryToCallSetupOrCleanup(benchmarkTypeInstance, benchmark.Descriptor.GlobalCleanupMethod, errors, cancellationToken).ConfigureAwait(); + await TryToCallSetupOrCleanup(benchmark, benchmarkTypeInstance, benchmark.Descriptor.GlobalCleanupMethod, errors, cancellationToken).ConfigureAwait(); } } @@ -73,9 +73,14 @@ protected override async IAsyncEnumerable ValidateAsyncCore(Val try { var workloadMethod = benchmark.Descriptor.WorkloadMethod; - var result = workloadMethod.Invoke(benchmarkTypeInstance, args); + if (workloadMethod.ReturnType.WithoutRefModifier().IsByRefLike()) + { + errors.Add(new ValidationError(false, $"Benchmark '{benchmark.DisplayInfo}' returns by-ref-like value, skipping return value validation.", benchmark)); + return default; + } if (workloadMethod.ReturnType.IsAwaitable(out var awaitableInfo)) { + var result = workloadMethod.Invoke(benchmarkTypeInstance, args); if (result is null) { errors.Add(new ValidationError(TreatsWarningsAsErrors, $"Awaitable benchmark '{benchmark.DisplayInfo}' returned null", benchmark)); @@ -85,15 +90,28 @@ protected override async IAsyncEnumerable ValidateAsyncCore(Val } else if (workloadMethod.ReturnType.IsAsyncEnumerable(out var asyncEnumerableInfo)) { + if (asyncEnumerableInfo.CurrentProperty.PropertyType.IsByRefLike()) + { + errors.Add(new ValidationError(false, $"Async enumerable benchmark '{benchmark.DisplayInfo}' yields by-ref-like elements, skipping return value validation.", benchmark)); + return default; + } + var result = workloadMethod.Invoke(benchmarkTypeInstance, args); if (result is null) { errors.Add(new ValidationError(TreatsWarningsAsErrors, $"Async enumerable benchmark '{benchmark.DisplayInfo}' returned null", benchmark)); return default; } - return (true, await DynamicAwaitHelper.ToListAsync(result, asyncEnumerableInfo).ConfigureAwait(false)); + List items = []; + // Mirrors real benchmark execution. + await foreach (var item in DynamicAwaitHelper.EnumerateBenchmarkAsync(result, asyncEnumerableInfo).ConfigureAwait(false)) + { + items.Add(item); + } + return (true, items); } else { + var result = workloadMethod.Invoke(benchmarkTypeInstance, args); return (workloadMethod.ReturnType != typeof(void), result); } } diff --git a/src/BenchmarkDotNet/Validators/SourceReturnTypeValidator.cs b/src/BenchmarkDotNet/Validators/SourceReturnTypeValidator.cs new file mode 100644 index 0000000000..ace5745a1c --- /dev/null +++ b/src/BenchmarkDotNet/Validators/SourceReturnTypeValidator.cs @@ -0,0 +1,73 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Extensions; +using System.Reflection; + +namespace BenchmarkDotNet.Validators; + +/// +/// Validates that every [ParamsSource]/[ArgumentsSource] member is one BenchmarkDotNet can reach and read values +/// from. The runtime counterpart of the BDN1306, BDN1308, BDN1311 and BDN1504 analyzer rules. +/// +public class SourceReturnTypeValidator : IValidator +{ + public static readonly SourceReturnTypeValidator FailOnError = new(); + + public bool TreatsWarningsAsErrors => true; + + public IAsyncEnumerable ValidateAsync(ValidationParameters input) + { + var fromParams = input.Benchmarks + .Select(benchmark => benchmark.Descriptor.Type) + .Distinct() + .SelectMany(ValidateParamsSources); + + var fromArguments = input.Benchmarks + .Select(benchmark => (benchmark.Descriptor.Type, benchmark.Descriptor.WorkloadMethod)) + .Distinct() + .SelectMany(descriptor => ValidateArgumentsSource(descriptor.Type, descriptor.WorkloadMethod)); + + return fromParams.Concat(fromArguments).ToAsyncEnumerable(); + } + + private IEnumerable ValidateParamsSources(Type type) + { + foreach (var member in type.GetTypeMembersWithGivenAttribute(ReflectionExtensions.ParameterMemberFlags)) + { + if (Validate(member.Attribute.Type ?? type, member.Attribute.Name, nameof(ParamsSourceAttribute), $"{type.Name}.{member.Name}") is { } error) + { + yield return error; + } + } + } + + private IEnumerable ValidateArgumentsSource(Type type, MethodInfo benchmark) + { + if (benchmark.ResolveAttribute() is { } attribute + && Validate(attribute.Type ?? type, attribute.Name, nameof(ArgumentsSourceAttribute), $"{type.Name}.{benchmark.Name}") is { } error) + { + yield return error; + } + } + + private ValidationError? Validate(Type sourceType, string sourceName, string attributeName, string owner) + { + var source = sourceType.FindSourceMember(sourceName); + if (source == null) + return null; + + string attributeText = $"[{attributeName.Replace(nameof(Attribute), "")}({sourceName})]"; + + var returnType = source.GetSourceReturnType(); + string prefix = $"Unable to use {owner} with {attributeText} because {sourceType.Name}.{sourceName} returns " + + $"{returnType.GetCorrectCSharpTypeName(includeNamespace: false, includeGenericArgumentsNamespace: false, prefixWithGlobal: false)}"; + + return returnType.CountSourceShapes() switch + { + 0 => new ValidationError(TreatsWarningsAsErrors, + $"{prefix}, which is neither IEnumerable nor IAsyncEnumerable. The non-generic IEnumerable is not enough on its own. Please, return IEnumerable or IAsyncEnumerable."), + > 1 => new ValidationError(TreatsWarningsAsErrors, + $"{prefix}, which has more than one enumerable shape, so BenchmarkDotNet cannot tell which one to read the values from. Please, return a single IEnumerable or IAsyncEnumerable."), + _ => null + }; + } +} diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs index 58db3ff85b..106cb36a78 100644 --- a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ArgumentsAttributeAnalyzerTests.cs @@ -1,5 +1,7 @@ +using BenchmarkDotNet.Analyzers; using BenchmarkDotNet.Analyzers.Attributes; using BenchmarkDotNet.Analyzers.Tests.Fixtures; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Xunit; @@ -81,6 +83,29 @@ public class RequiresBenchmarkAttribute : AnalyzerTestFixture GenerateData() } } } + + public class SourceElementMustNotBeByRefLike : AnalyzerTestFixture + { + public SourceElementMustNotBeByRefLike() : base(AnalyzerHelper.SourceElementMustNotBeByRefLikeRule) { } + + [Fact] + public async Task ASourceYieldingARefStruct_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using System; + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable> Values() => null; + + [Benchmark] + [ArgumentsSource({|#0:nameof(Values)|})] + public void Run(Span a) { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "System.Span", "is a ref struct"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + + // On the derived type the argument is fixed, so the constraint stops deciding anything. + [Fact] + public async Task ASourceClosedByTheDerivedTypeToAValueType_ShouldNotReportError() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public abstract class BaseClass where T : allows ref struct + { + public static IEnumerable Values() => null; + } + + public class BenchmarkClass : BaseClass + { + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(int a) { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task ASourceClosedByTheDerivedTypeToARefStruct_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using System; + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public abstract class BaseClass where T : allows ref struct + { + public static IEnumerable Values() => null; + } + + public class BenchmarkClass : BaseClass> + { + [Benchmark] + [ArgumentsSource({|#0:nameof(Values)|})] + public void Run(ReadOnlySpan a) { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "System.ReadOnlySpan", "is a ref struct"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + // A ref struct *parameter* stays supported, fed from what the value is built from. + [Fact] + public async Task ARefStructParameterFedFromAnArray_ShouldNotReportError() + { + var testCode = /* lang=c#-test */ """ + using System; + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values() => null; + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(ReadOnlySpan a) { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + } + + public class SourceElementMayBeByRefLike : AnalyzerTestFixture + { + public SourceElementMayBeByRefLike() : base(AnalyzerHelper.SourceElementMayBeByRefLikeRule) { } + + // Read where the attribute is: on the open type the element is the type parameter, and a constraint + // admitting a ref struct guarantees nothing about boxing. + [Fact] + // A constraint that admits a ref struct does not say this source fails - the substitution decides, and one + // that is not by-ref-like reads fine at run time. The compiler cannot see which, so this warns rather than + // refusing code that runs; a concrete ref struct stays an error. + public async Task ASourceYieldingAParameterAdmittingARefStruct_ShouldReportWarning() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public abstract class BaseClass where T : allows ref struct + { + public static IEnumerable Values() => null; + + [Benchmark] + [ArgumentsSource({|#0:nameof(Values)|})] + public void Run(T a) { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Warning, "Values", "T", "admits a ref struct"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + } + + public class SourceMethodMustNotBeGeneric : AnalyzerTestFixture + { + public SourceMethodMustNotBeGeneric() : base(AnalyzerHelper.SourceMethodMustNotBeGenericRule) { } + + [Fact] + public async Task AGenericSourceMethod_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values() => null; + + [Benchmark] + [ArgumentsSource({|#0:nameof(Values)|})] + public void Run(int a) { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task AGenericSourceMethodWithANonGenericOverload_ShouldNotReportError() + { + // The runtime invokes the non-generic overload, so there is nothing to report. + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values() => null; + + public static IEnumerable Values() => null; + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(int a) { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + } + + public class ArgumentsSourceMustReturnEnumerable : AnalyzerTestFixture + { + public ArgumentsSourceMustReturnEnumerable() : base(ArgumentsAttributeAnalyzer.ArgumentsSourceMustReturnEnumerableRule) { } + + [Theory] + [InlineData("System.Collections.Generic.IEnumerable Values() => null;")] + [InlineData("System.Collections.Generic.IEnumerable Values() => null;")] + [InlineData("object[][] Values() => null;")] + [InlineData("System.Collections.Generic.IAsyncEnumerable Values() => null;")] + public async Task SupportedReturnType_ShouldNotReportError(string sourceMember) + { + var testCode = /* lang=c#-test */ $$""" + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static {{sourceMember}} + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(int a) { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task NonEnumerableReturnType_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static int Values() => 0; + + [Benchmark] + [ArgumentsSource({|#0:nameof(Values)|})] + public void Run(int a) { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "int"); + + await RunAsync(); + } + + [Fact] + public async Task A_derived_attribute_naming_its_source_through_the_base_constructor_does_not_report_error() + { + // The runtime reads the Name property, which base(...) set. This usage's own argument is a label that + // happens to match a real member, so reading it as the source name resolves the wrong one and reports + // against a member the benchmark never uses. + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class LabelledArgumentsSourceAttribute : ArgumentsSourceAttribute + { + public LabelledArgumentsSourceAttribute(string label) : base("Values") { } + } + + public class BenchmarkClass + { + public int Label => 0; + + public static IEnumerable Values() => null; + + [Benchmark] + [LabelledArgumentsSource("Label")] + public void Run(int a) { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task NonGenericEnumerableReturnType_ShouldReportError() + { + // The generated code infers the element type from the source, so a type that only implements the + // non-generic IEnumerable gives inference nothing to bind to and fails to compile (CS0411). + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static System.Collections.IEnumerable Values() => null; + + [Benchmark] + [ArgumentsSource({|#0:nameof(Values)|})] + public void Run(int a) { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "System.Collections.IEnumerable"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task NonEnumerableReturnType_FromBaseClass_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BaseClass + { + public static int Values() => 0; + } + + public class BenchmarkClass : BaseClass + { + [Benchmark] + [ArgumentsSource({|#0:nameof(Values)|})] + public void Run(int a) { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "int"); + + await RunAsync(); + } + + [Fact] + public async Task EnumerableReturnType_FromBaseClass_ShouldNotReportError() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BaseClass + { + public static IEnumerable Values() => null; + } + + public class BenchmarkClass : BaseClass + { + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(int a) { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task NonEnumerableReturnType_FromOtherType_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class Source + { + public static int Values() => 0; + } + + public class BenchmarkClass + { + [Benchmark] + [ArgumentsSource(typeof(Source), {|#0:nameof(Source.Values)|})] + public void Run(int a) { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "int"); + + await RunAsync(); + } + } + + public class ArgumentsSourceMethodRequiresOptionalParameters : AnalyzerTestFixture + { + public ArgumentsSourceMethodRequiresOptionalParameters() : base(AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule) { } + + [Fact] + public async Task A_source_method_with_a_required_parameter_reports_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values(int count) => null; + + [Benchmark] + [ArgumentsSource({|#0:nameof(Values)|})] + public void Run(int a) { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_source_method_with_only_optional_parameters_does_not_report_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values(int count = 1) => null; + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(int a) { } + } + """; + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/GeneralParameterAttributesAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/GeneralParameterAttributesAnalyzerTests.cs index 16bf923259..1b7fd8c81c 100644 --- a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/GeneralParameterAttributesAnalyzerTests.cs +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/GeneralParameterAttributesAnalyzerTests.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Analyzers; using BenchmarkDotNet.Analyzers.Attributes; using BenchmarkDotNet.Analyzers.Tests.Fixtures; using Microsoft.CodeAnalysis; @@ -158,6 +159,37 @@ public class BenchmarkClass await RunAsync(); } + // Two *different* classes from one parameter-attribute family. The compiler says nothing about them - + // CS0579 covers only the same class applied twice - so the duplicate has to be reported here. This also + // guards the whole method: the duplicate check used to bail out on this shape, taking every other + // diagnostic for the member with it, so a private field carrying them compiled clean. + [Fact] + public async Task A_field_annotated_with_two_different_classes_from_one_family_should_trigger_diagnostic() + { + const string testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class CustomParamsAttribute : ParamsAttribute + { + public CustomParamsAttribute(params object[] values) : base(values) { } + } + + public class BenchmarkClass + { + [{|#0:Params(1)|}] + [{|#1:CustomParams(2)|}] + private int _field = 0; + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, "_field"); + AddExpectedDiagnostic(1, "_field"); + DisableCompilerDiagnostics(); + + await RunAsync(); + } + public static TheoryData UniqueParameterAttributeUsages => [.. UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)]; @@ -964,168 +996,6 @@ public static TheoryData DuplicateParameterAttributeUsageCounts => DuplicateAttributeUsageCountsTheoryData; } -#if NET5_0_OR_GREATER - public class PropertyCannotBeInitOnly : AnalyzerTestFixture - { - public PropertyCannotBeInitOnly() : base(GeneralParameterAttributesAnalyzer.PropertyCannotBeInitOnlyRule) { } - - [Fact] - public async Task An_initonly_property_not_annotated_with_any_parameter_attribute_should_not_trigger_diagnostic() - { - const string testCode = /* lang=c#-test */ """ - public class BenchmarkClass - { - public int Property { get; init; } - } - """; - - TestCode = testCode; - - await RunAsync(); - } - - [Fact] - public async Task An_initonly_property_annotated_with_a_nonparameter_attribute_should_not_trigger_diagnostic() - { - const string testCode = /* lang=c#-test */ """ - public class BenchmarkClass - { - [Dummy] - public int Property { get; init; } - } - """; - - TestCode = testCode; - ReferenceDummyAttribute(); - - await RunAsync(); - } - - [Theory] - [MemberData(nameof(UniqueParameterAttributeUsages))] - public async Task A_property_with_an_assignable_setter_annotated_with_a_unique_parameter_attribute_should_not_trigger_diagnostic(string attributeUsage) - { - var testCode = /* lang=c#-test */ $$""" - using BenchmarkDotNet.Attributes; - - public class BenchmarkClass - { - [{{attributeUsage}}] - public int Property { get; set; } - } - """; - - TestCode = testCode; - - await RunAsync(); - } - - [Theory] - [MemberData(nameof(DuplicateSameParameterAttributeUsages))] - public async Task An_initonly_property_annotated_with_the_same_duplicate_parameter_attribute_should_not_trigger_diagnostic( - string currentAttributeUsage, - int currentUniqueAttributeUsagePosition, - int[] duplicateSameAttributeUsageCounts) - { - var duplicateAttributeUsages = new List(1 + duplicateSameAttributeUsageCounts.Sum()); - - var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); - - for (var i = 0; i < duplicateSameAttributeUsageCounts.Length; i++) - { - if (i == currentUniqueAttributeUsagePosition) - { - duplicateAttributeUsages.Add($"[{currentAttributeUsage}]"); - } - - for (var j = 0; j < duplicateSameAttributeUsageCounts[i]; j++) - { - duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); - } - } - - var testCode = /* lang=c#-test */ $$""" - using BenchmarkDotNet.Attributes; - - public class BenchmarkClass - { - {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} - public int Property { get; init; } - } - """; - - TestCode = testCode; - DisableCompilerDiagnostics(); - - await RunAsync(); - } - - [Theory] - [MemberData(nameof(DuplicateParameterAttributeUsageCounts))] - public async Task An_initonly_property_annotated_with_more_than_one_parameter_attribute_should_not_trigger_diagnostic(int[] duplicateAttributeUsageCounts) - { - var duplicateAttributeUsages = new List(duplicateAttributeUsageCounts.Sum()); - - var uniqueParameterAttributeUsages = UniqueParameterAttributeUsages.AsReadOnly(); - - for (var i = 0; i < duplicateAttributeUsageCounts.Length; i++) - { - for (var j = 0; j < duplicateAttributeUsageCounts[i]; j++) - { - duplicateAttributeUsages.Add($"[{uniqueParameterAttributeUsages[i]}]"); - } - } - - var testCode = /* lang=c#-test */ $$""" - using BenchmarkDotNet.Attributes; - - public class BenchmarkClass - { - {{string.Join($"{Environment.NewLine} ", duplicateAttributeUsages)}} - public int Property { get; init; } - } - """; - - TestCode = testCode; - - await RunAsync(); - } - - [Theory] - [MemberData(nameof(UniqueParameterAttributes))] - public async Task An_initonly_property_annotated_with_a_unique_parameter_attribute_should_trigger_diagnostic(string attributeName, string attributeUsage) - { - const string propertyIdentifier = "Property"; - - var testCode = /* lang=c#-test */ $$""" - using BenchmarkDotNet.Attributes; - - public class BenchmarkClass - { - [{{attributeUsage}}] - public int {{propertyIdentifier}} { get; {|#0:init|}; } - } - """; - - TestCode = testCode; - AddDefaultExpectedDiagnostic(propertyIdentifier, attributeName); - - await RunAsync(); - } - - public static TheoryData UniqueParameterAttributeUsages - => [.. UniqueParameterAttributesTheoryData.Select(tdr => (tdr[1] as string)!)]; - - public static TheoryData UniqueParameterAttributes - => UniqueParameterAttributesTheoryData; - - public static TheoryData DuplicateSameParameterAttributeUsages - => DuplicateSameAttributeUsagesTheoryData; - - public static TheoryData DuplicateParameterAttributeUsageCounts - => DuplicateAttributeUsageCountsTheoryData; - } -#endif public class PropertyMustHavePublicSetter : AnalyzerTestFixture { public PropertyMustHavePublicSetter() : base(GeneralParameterAttributesAnalyzer.PropertyMustHavePublicSetterRule) { } @@ -1443,6 +1313,788 @@ public void Run() { } } } + public class ParamsSourceMustReturnEnumerable : AnalyzerTestFixture + { + public ParamsSourceMustReturnEnumerable() : base(GeneralParameterAttributesAnalyzer.ParamsSourceMustReturnEnumerableRule) { } + + [Theory] + [InlineData("System.Collections.Generic.IEnumerable Values() => null;")] + [InlineData("int[] Values() => null;")] + [InlineData("System.Collections.Generic.List Values() => null;")] + [InlineData("System.Collections.Generic.IAsyncEnumerable Values() => null;")] + [InlineData("System.Collections.Generic.IEnumerable Values => null;")] + public async Task SupportedReturnType_ShouldNotReportError(string sourceMember) + { + var testCode = /* lang=c#-test */ $$""" + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static {{sourceMember}} + + [ParamsSource(nameof(Values))] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task CustomAsyncEnumerablePattern_ShouldReportError() + { + // The await-foreach pattern without the IAsyncEnumerable interface is not supported. + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using System.Threading.Tasks; + using BenchmarkDotNet.Attributes; + + public sealed class CustomAsyncEnumerable + { + public CustomAsyncEnumerator GetAsyncEnumerator() => new(); + } + + public sealed class CustomAsyncEnumerator + { + public int Current => 0; + public ValueTask MoveNextAsync() => new(false); + } + + public class BenchmarkClass + { + public static CustomAsyncEnumerable Values() => new(); + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "CustomAsyncEnumerable"); + // ValueTask isn't resolvable in the net472 test compilation; the analyzer only inspects the source's + // declared return type, so the compiler diagnostics are irrelevant here (matches SupportedReturnType). + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_derived_attribute_naming_its_source_through_the_base_constructor_does_not_report_error() + { + // The runtime reads the Name property, which base(...) set. This usage's own argument is a label that + // happens to match a real member, so reading it as the source name resolves the wrong one and reports + // against a member the benchmark never uses. + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class LabelledParamsSourceAttribute : ParamsSourceAttribute + { + public LabelledParamsSourceAttribute(string label) : base("Values") { } + } + + public class BenchmarkClass + { + public int Label => 0; + + public static IEnumerable Values() => null; + + [LabelledParamsSource("Label")] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task NonEnumerableReturnType_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static int Values() => 0; + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "int"); + + await RunAsync(); + } + + [Theory] + [InlineData("System.Collections.IEnumerable Values() => null;", "System.Collections.IEnumerable")] + [InlineData("System.Collections.ArrayList Values() => null;", "System.Collections.ArrayList")] + public async Task NonGenericEnumerableReturnType_ShouldReportError(string sourceMember, string returnTypeName) + { + // The generated code infers the element type from the source, so a type that only implements the + // non-generic IEnumerable gives inference nothing to bind to and fails to compile (CS0411). + var testCode = /* lang=c#-test */ $$""" + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static {{sourceMember}} + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", returnTypeName); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task TaskOfEnumerableReturnType_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using System.Threading.Tasks; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static Task> Values() => null; + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "System.Threading.Tasks.Task>"); + + await RunAsync(); + } + + [Fact] + public async Task NonEnumerableReturnType_FromBaseClass_ShouldReportError() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BaseClass + { + public static int Values() => 0; + } + + public class BenchmarkClass : BaseClass + { + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "int"); + + await RunAsync(); + } + + [Fact] + public async Task EnumerableReturnType_OnParameterlessOverload_ShouldNotReportError() + { + // The overload with a required parameter is not the one BDN invokes, so its return type is irrelevant. + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static int Values(int count) => count; + public static IEnumerable Values() => new[] { 1, 2, 3 }; + + [ParamsSource(nameof(Values))] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + await RunAsync(); + } + + [Fact] + public async Task EnumerableReturnType_FromBaseClass_ShouldNotReportError() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BaseClass + { + public static IEnumerable Values() => new[] { 1, 2, 3 }; + } + + public class BenchmarkClass : BaseClass + { + [ParamsSource(nameof(Values))] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + await RunAsync(); + } + } + + public class ReservedMemberName : AnalyzerTestFixture + { + public ReservedMemberName() : base(GeneralParameterAttributesAnalyzer.ReservedMemberNameRule) { } + + [Fact] + public async Task A_params_property_named_like_a_generated_member_reports_error() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Params(1)] + public int {|#0:__GlobalSetup|} { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "__GlobalSetup", "Params"); + await RunAsync(); + } + + [Fact] + public async Task A_params_field_named_like_a_generated_member_reports_error() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Params(1)] + public int {|#0:__fieldsContainer|} = 0; + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "__fieldsContainer", "Params"); + await RunAsync(); + } + + [Fact] + public async Task A_params_source_property_named_like_a_generated_member_reports_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values() => new[] { 1, 2, 3 }; + + [ParamsSource(nameof(Values))] + public int {|#0:__WorkloadActionUnroll|} { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "__WorkloadActionUnroll", "ParamsSource"); + await RunAsync(); + } + + [Theory] + [InlineData("__Run")] // the generated runnable's static entry-point method + [InlineData("__FieldsContainer")] // the generated nested arguments struct + public async Task A_params_member_named_like_a_generated_entry_point_or_container_reports_error(string name) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Params(1)] + public int {|#0:{{name}}|} { get; set; } + + [Benchmark] + public void Run2() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, name, "Params"); + await RunAsync(); + } + + [Theory] + [InlineData("OverheadActionUnroll")] + [InlineData("Run")] // the entry point is __Run, so plain Run is the benchmark's to use + [InlineData("FieldsContainer")] // likewise the arguments struct is __FieldsContainer + public async Task A_params_member_named_like_a_freed_template_name_does_not_report_error(string name) + { + // The un-prefixed template names are free to use now that generated members are __-prefixed. #2821 + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Params(1)] + public int {{name}} { get; set; } + + [Benchmark] + public void Benchmark() { } + } + """; + TestCode = testCode; + await RunAsync(); + } + + [Fact] + public async Task A_non_parameter_member_named_like_a_generated_member_does_not_report_error() + { + // Non-parameter members are reached via hiding, not an object initializer, so they don't collide. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public int __GlobalSetup { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + await RunAsync(); + } + } + + public class ParamsSourceMethodRequiresOptionalParameters : AnalyzerTestFixture + { + public ParamsSourceMethodRequiresOptionalParameters() : base(AnalyzerHelper.SourceMethodMustNotHaveRequiredParametersRule) { } + + [Fact] + public async Task A_source_method_with_a_required_parameter_reports_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values(int count) => new[] { count }; + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values"); + await RunAsync(); + } + + [Fact] + public async Task A_source_method_with_only_optional_parameters_does_not_report_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values(int count = 1) => new[] { count }; + + [ParamsSource(nameof(Values))] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + await RunAsync(); + } + + [Fact] + public async Task An_overload_with_required_parameters_does_not_report_error_when_a_parameterless_one_exists() + { + // BDN invokes the parameterless overload, so the source is valid. + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable Values(int count) => new[] { count }; + public static IEnumerable Values() => new[] { 1 }; + + [ParamsSource(nameof(Values))] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + await RunAsync(); + } + } + + public class ReservedNameAcrossDeclarators : AnalyzerTestFixture + { + public ReservedNameAcrossDeclarators() : base(GeneralParameterAttributesAnalyzer.ReservedMemberNameRule) { } + + [Fact] + public async Task AReservedNameOnALaterDeclarator_ShouldReportError() + { + // One declaration, several members - the attribute applies to each, and the runnable's object + // initializer has to bind each. Checking only the first leaves the rest unreported. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Params(1)] + public int Fine, {|#0:__Overhead|}; + + [Benchmark] + public int Run() => Fine; + } + """; + + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "__Overhead", "Params"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + } + + public class SourceElementMustNotBeByRefLike : AnalyzerTestFixture + { + public SourceElementMustNotBeByRefLike() : base(AnalyzerHelper.SourceElementMustNotBeByRefLikeRule) { } + + [Fact] + public async Task A_source_yielding_a_ref_struct_reports_error() + { + var testCode = /* lang=c#-test */ """ + using System; + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IEnumerable> Values() => null; + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "System.Span", "is a ref struct"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + + // On the derived type the argument is fixed, so the constraint stops deciding anything. + [Fact] + public async Task A_source_closed_by_the_derived_type_to_a_value_type_does_not_report_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public abstract class BaseClass where T : allows ref struct + { + public static IEnumerable Values() => null; + } + + public class BenchmarkClass : BaseClass + { + [ParamsSource(nameof(Values))] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_source_closed_by_the_derived_type_to_a_ref_struct_reports_error() + { + var testCode = /* lang=c#-test */ """ + using System; + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public abstract class BaseClass where T : allows ref struct + { + public static IEnumerable Values() => null; + } + + public class BenchmarkClass : BaseClass> + { + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "System.ReadOnlySpan", "is a ref struct"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + } + + public class SourceElementMayBeByRefLike : AnalyzerTestFixture + { + public SourceElementMayBeByRefLike() : base(AnalyzerHelper.SourceElementMayBeByRefLikeRule) { } + + // Read where the attribute is: on the open type the element is the type parameter, and a constraint + // admitting a ref struct guarantees nothing about boxing. + [Fact] + // A constraint that admits a ref struct does not say this source fails - the substitution decides, and one + // that is not by-ref-like reads fine at run time. The compiler cannot see which, so this warns rather than + // refusing code that runs; a concrete ref struct stays an error. + public async Task A_source_yielding_a_parameter_admitting_a_ref_struct_reports_warning() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public abstract class BaseClass where T : allows ref struct + { + public static IEnumerable Values() => null; + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Warning, "Values", "T", "admits a ref struct"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + } + + public class SourceMustNotBeAmbiguouslyEnumerable : AnalyzerTestFixture + { + public SourceMustNotBeAmbiguouslyEnumerable() : base(AnalyzerHelper.SourceMustNotBeAmbiguouslyEnumerableRule) { } + + [Fact] + public async Task A_source_that_is_both_shapes_reports_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections; + using System.Collections.Generic; + using System.Threading; + using BenchmarkDotNet.Attributes; + + public class BothShapes : IEnumerable, IAsyncEnumerable + { + public IEnumerator GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => null; + } + + public class BenchmarkClass + { + public static BothShapes Values() => null; + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "BothShapes"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_source_that_is_only_an_async_enumerable_does_not_report_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public static IAsyncEnumerable Values() => null; + + [ParamsSource(nameof(Values))] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_source_with_several_enumerable_instantiations_reports_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections; + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class TwoElementTypes : IEnumerable, IEnumerable + { + IEnumerator IEnumerable.GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + } + + public class BenchmarkClass + { + public static TwoElementTypes Values() => null; + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "TwoElementTypes"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_source_with_several_async_enumerable_instantiations_reports_error() + { + var testCode = /* lang=c#-test */ """ + using System.Collections.Generic; + using System.Threading; + using BenchmarkDotNet.Attributes; + + public class TwoAsyncElementTypes : IAsyncEnumerable, IAsyncEnumerable + { + IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) => null; + IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) => null; + } + + public class BenchmarkClass + { + public static TwoAsyncElementTypes Values() => null; + + [ParamsSource({|#0:nameof(Values)|})] + public int MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "TwoAsyncElementTypes"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_source_whose_element_types_are_convertible_still_reports_error() + { + // Type inference needs a *unique* candidate interface, so it fails here too - it does not quietly + // settle on object. This is why the rule counts instantiations instead of asking whether one element + // type is assignable to the other. + var testCode = /* lang=c#-test */ """ + using System.Collections; + using System.Collections.Generic; + using BenchmarkDotNet.Attributes; + + public class StringAndObject : IEnumerable, IEnumerable + { + IEnumerator IEnumerable.GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + } + + public class BenchmarkClass + { + public static StringAndObject Values() => null; + + [ParamsSource({|#0:nameof(Values)|})] + public string MyParam { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Values", "StringAndObject"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + } + public static TheoryData UniqueParameterAttributesTheoryData => new() { diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs index 5b2bb6cc86..1070aaa466 100644 --- a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/Attributes/ParamsAttributeAnalyzerTests.cs @@ -41,6 +41,59 @@ public class MustHaveValues : AnalyzerTestFixture { public MustHaveValues() : base(ParamsAttributeAnalyzer.MustHaveValuesRule) { } + [Fact] + public async Task A_derived_attribute_passing_values_to_the_base_constructor_does_not_report_error() + { + // The values reach ParamsAttribute through base(...), where this analyzer cannot see them, so the + // usage's own empty argument list must not be read as "no values". + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BoolParamsAttribute : ParamsAttribute + { + public BoolParamsAttribute() : base(true, false) { } + } + + public class BenchmarkClass + { + [BoolParams] + public bool Value { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + await RunAsync(); + } + + [Fact] + public async Task A_derived_attribute_with_its_own_constructor_shape_does_not_report_error() + { + // A scalar constructor argument is not the base's object[] of values; reading it as one throws. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class RangeParamsAttribute : ParamsAttribute + { + public RangeParamsAttribute(int max) : base(0, max) { } + } + + public class BenchmarkClass + { + [RangeParams(5)] + public int Value { get; set; } + + [Benchmark] + public void Run() { } + } + """; + + TestCode = testCode; + await RunAsync(); + } + [Theory, CombinatorialData] public async Task Providing_one_or_more_values_should_not_trigger_diagnostic( [CombinatorialMemberData(nameof(FieldOrPropertyDeclarations))] string fieldOrPropertyDeclaration, diff --git a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/General/BenchmarkClassAnalyzerTests.cs b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/General/BenchmarkClassAnalyzerTests.cs index d9dfcefe83..c5241aa9b5 100644 --- a/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/General/BenchmarkClassAnalyzerTests.cs +++ b/tests/BenchmarkDotNet.Analyzers.Tests/AnalyzerTests/General/BenchmarkClassAnalyzerTests.cs @@ -210,6 +210,36 @@ public void BenchmarkMethod() await RunAsync(); } + // A derived attribute's own arguments are not the type arguments - it hands those to base(...), out of sight + // here - so counting them reports a class the runtime builds fine. + [Theory, CombinatorialData] + public async Task Generic_class_annotated_with_a_derived_generictypearguments_attribute_should_not_trigger_diagnostic( + [CombinatorialMemberData(nameof(BenchmarkAttributeUsagesEnumerableLocal))] string benchmarkAttributeUsage) + { + var testCode = /* lang=c#-test */ $$""" + using BenchmarkDotNet.Attributes; + + public class TwoTypeArgumentsAttribute : GenericTypeArgumentsAttribute + { + public TwoTypeArgumentsAttribute(string label) : base(typeof(int), typeof(string)) { } + } + + [TwoTypeArguments("label")] + public class BenchmarkClass + { + {{benchmarkAttributeUsage}} + public void BenchmarkMethod() + { + + } + } + """; + + TestCode = testCode; + + await RunAsync(); + } + public static IEnumerable> TypeArgumentsData => [ ("typeof(int), typeof(string)", 2), @@ -423,6 +453,32 @@ public SingleNullArgumentToBenchmarkCategoryAttributeNotAllowed() : base(Benchma { } + [Fact] + public async Task A_null_argument_to_a_derived_category_attribute_does_not_report_error() + { + // The rule is about a null *category*. A derived attribute's single argument is whatever its own + // constructor takes, and the category it forwards to base(...) is not null at all. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class LabelledCategoryAttribute : BenchmarkCategoryAttribute + { + public LabelledCategoryAttribute(string label) : base("fixed") { } + } + + public class BenchmarkClass + { + [Benchmark] + [LabelledCategory(null)] + public void Run() { } + } + """; + + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + [Theory, CombinatorialData] public async Task Providing_a_non_null_single_argument_should_not_trigger_diagnostic( [CombinatorialMemberData(nameof(ClassAbstractModifiersEnumerableLocal))] string abstractModifier, @@ -1154,6 +1210,74 @@ public class OnlyOneMethodCanBeBaselinePerCategory : AnalyzerTestFixture + { + public RequiredMemberCannotBeSet() : base(RequiredMemberAnalyzer.RequiredMemberCannotBeSetRule) { } + + [Fact] + public async Task A_required_member_without_a_settable_attribute_reports_error() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public required string {|#0:Text|} { get; set; } + + [GlobalSetup] + public void Setup() => Text = ""; + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Text"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_required_field_without_a_settable_attribute_reports_error() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + public required int {|#0:Value|}; + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Value"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_required_params_member_does_not_report_error() + { + // [Params*] members are set in the runnable's object initializer, so `required` is satisfied. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Params(1)] + public required int Value { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_required_benchmark_cancellation_member_does_not_report_error() + { + // An instance [BenchmarkCancellation] member is set in the cancellation-token initializer. + var testCode = /* lang=c#-test */ """ + using System.Threading; + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [BenchmarkCancellation] + public required CancellationToken Token { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_required_member_inherited_from_a_non_benchmark_base_reports_error() + { + // The base has no benchmarks, but its required member is inherited by the benchmark type (and the runnable). + // The diagnostic is reported at the benchmark class's `: BaseType` reference, not the base's declaration. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkBase + { + public required string Text { get; set; } + } + + public class BenchmarkClass : {|#0:BenchmarkBase|} + { + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Text"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_required_member_on_a_benchmark_base_is_reported_once_at_its_declaration() + { + // Both types are benchmark types (the derived inherits [Benchmark]); the required member is flagged only + // once, at its declaration on the base - not again at the derived type's base-type reference. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BaseBench + { + public required string {|#0:Text|} { get; set; } + + [Benchmark] + public void Run() { } + } + + public class DerivedBench : BaseBench + { + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Text"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_SetsRequiredMembers_ctor_does_not_suppress_the_member_diagnostic() + { + // BDN reports the constructor separately (BDN1110) rather than propagating the attribute, so the member + // BDN cannot set is still flagged. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + using System.Diagnostics.CodeAnalysis; + + namespace System.Diagnostics.CodeAnalysis + { + internal sealed class SetsRequiredMembersAttribute : System.Attribute { } + } + + public class BenchmarkClass + { + public required string {|#0:Text|} { get; set; } + + [SetsRequiredMembers] + public BenchmarkClass() { Text = ""; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Text"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_type_whose_benchmarks_use_a_derived_benchmark_attribute_is_analyzed() + { + // The runtime resolves [Benchmark] with GetCustomAttributes, so a user's own attribute deriving from it + // still makes the type a benchmark - and its required members still have to be settable. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class CustomBenchmarkAttribute : BenchmarkAttribute { } + + public class BenchmarkClass + { + public required string {|#0:Text|} { get; set; } + + [CustomBenchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "Text"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_required_member_with_a_derived_params_attribute_does_not_report_error() + { + // BenchmarkDotNet resolves its attributes with GetCustomAttributes, which matches derived types, so a + // user's own attribute deriving from [Params] is still set at construction. + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class CustomParamsAttribute : ParamsAttribute + { + public CustomParamsAttribute(params object[] values) : base(values) { } + } + + public class BenchmarkClass + { + [CustomParams(1)] + public required int Value { get; set; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_required_member_in_a_non_benchmark_type_does_not_report_error() + { + var testCode = /* lang=c#-test */ """ + public class NotABenchmark + { + public required string Text { get; set; } + } + """; + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + } + + public class ConstructorMustNotSetRequiredMembers : AnalyzerTestFixture + { + public ConstructorMustNotSetRequiredMembers() : base(RequiredMemberAnalyzer.ConstructorMustNotSetRequiredMembersRule) { } + + [Fact] + public async Task A_SetsRequiredMembers_ctor_reports_error() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + using System.Diagnostics.CodeAnalysis; + + namespace System.Diagnostics.CodeAnalysis + { + internal sealed class SetsRequiredMembersAttribute : System.Attribute { } + } + + public class BenchmarkClass + { + [Params(1)] + public required int Value { get; set; } + + [SetsRequiredMembers] + public {|#0:BenchmarkClass|}() { Value = 1; } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + AddExpectedDiagnostic(0, DiagnosticSeverity.Error, "BenchmarkClass"); + DisableCompilerDiagnostics(); + await RunAsync(); + } + + [Fact] + public async Task A_plain_ctor_does_not_report_error() + { + var testCode = /* lang=c#-test */ """ + using BenchmarkDotNet.Attributes; + + public class BenchmarkClass + { + [Params(1)] + public int Value { get; set; } + + public BenchmarkClass() { } + + [Benchmark] + public void Run() { } + } + """; + TestCode = testCode; + await RunAsync(); + } + + [Fact] + public async Task A_SetsRequiredMembers_ctor_in_a_non_benchmark_type_does_not_report_error() + { + var testCode = /* lang=c#-test */ """ + using System.Diagnostics.CodeAnalysis; + + namespace System.Diagnostics.CodeAnalysis + { + internal sealed class SetsRequiredMembersAttribute : System.Attribute { } + } + + public class NotABenchmark + { + public required string Text { get; set; } + + [SetsRequiredMembers] + public NotABenchmark() { Text = ""; } + } + """; + TestCode = testCode; + DisableCompilerDiagnostics(); + await RunAsync(); + } + } +} diff --git a/tests/BenchmarkDotNet.IntegrationTests.SharedDiagnosers/FinalizerBlockerDiagnoser.cs b/tests/BenchmarkDotNet.IntegrationTests.SharedDiagnosers/FinalizerBlockerDiagnoser.cs index 9d5744162f..af22588b19 100644 --- a/tests/BenchmarkDotNet.IntegrationTests.SharedDiagnosers/FinalizerBlockerDiagnoser.cs +++ b/tests/BenchmarkDotNet.IntegrationTests.SharedDiagnosers/FinalizerBlockerDiagnoser.cs @@ -21,7 +21,15 @@ public sealed class FinalizerBlockerDiagnoser : IInProcessDiagnoser public IEnumerable Analysers => []; public void DeserializeResults(BenchmarkCase benchmarkCase, string serializedResults) { } public void DisplayResults(ILogger logger) { } - public IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) => AsyncEnumerable.Empty(); + // Not AsyncEnumerable.Empty: BenchmarkDotNet's polyfill for it is internal and compiled out of that assembly's + // .NET 10 asset, so binding it from this netstandard2.0 project resolves against the netstandard asset and then + // fails to load under a .NET 10 host. +#pragma warning disable CS1998 + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) + { + yield break; + } +#pragma warning restore CS1998 public IEnumerable ProcessResults(DiagnoserResults results) => []; public ValueTask HandleAsync(HostSignal signal, DiagnoserActionParameters parameters, CancellationToken cancellationToken) => new(); public RunMode GetRunMode(BenchmarkCase benchmarkCase) diff --git a/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs b/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs index 0b2465fc69..df5903df95 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/ArgumentsTests.cs @@ -133,6 +133,88 @@ public void OnePrimitiveAndOneNonPrimitive(Version version, int number) throw new InvalidOperationException("Incorrect values were passed"); } } + [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void ArgumentsFromSourceInAClosedGenericClassArePassedToBenchmarks(IToolchain toolchain) => CanExecute(toolchain); + + public class WithArgumentsSourceInAClosedGenericClass + { + [Benchmark] + [ArgumentsSource(typeof(ExternalGenericArgumentsSource), nameof(ExternalGenericArgumentsSource.SingleValue))] + public void SingleValue(int number) + { + if (number != 3) + throw new InvalidOperationException("Incorrect values were passed"); + } + + [Benchmark] + [ArgumentsSource(typeof(ExternalGenericArgumentsSource), nameof(ExternalGenericArgumentsSource.ArgumentList))] + public void ArgumentList(int number, string text) + { + if (number != 3 || text != "three") + throw new InvalidOperationException("Incorrect values were passed"); + } + } + + [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void ArgumentsTakenWholeByAnImplicitConversionArePassedToBenchmarks(IToolchain toolchain) => CanExecute(toolchain); + + // A parameter is handed the element only where it is declared as the element type, or is by-ref-like and + // built from it by a conversion no declaration expresses. ReadOnlySpan is the second; `ref byte[]` is the + // first, a ref modifier changing nothing. ReadOnlyMemory is the third case: it declares a conversion from + // byte[] and is not by-ref-like, so it has to name a source yielding what it takes. Memory and + // ArraySegment stand in the same place and would only repeat it with another type name. + public class WithArgumentsTakenWholeByConversion + { + public static IEnumerable Values() + { + yield return [1, 2, 3]; + } + + public static IEnumerable> ReadOnlyMemoryValues() + { + yield return new byte[] { 1, 2, 3 }; + } + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Span(ReadOnlySpan a) + { + if (a.Length != 3) + throw new InvalidOperationException("Incorrect values were passed"); + } + + [Benchmark] + [ArgumentsSource(nameof(ReadOnlyMemoryValues))] + public void ReadOnlyMemory(ReadOnlyMemory a) + { + if (a.Length != 3) + throw new InvalidOperationException("Incorrect values were passed"); + } + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void RefArray(ref byte[] a) + { + if (a.Length != 3) + throw new InvalidOperationException("Incorrect values were passed"); + } + } + + // The attribute closes the type, so what the source yields is fixed before anything reads it. The generated + // code has to name the closed type to call back into it. + public static class ExternalGenericArgumentsSource + { + public static IEnumerable SingleValue() + { + yield return (T)(object)3; + } + + public static IEnumerable ArgumentList() + { + yield return new object[] { 3, "three" }; + } + } + public static class ExternalClassWithArgumentsSource { public static IEnumerable OnePrimitiveType() diff --git a/tests/BenchmarkDotNet.IntegrationTests/AsyncEnumerableParamsSourceTests.cs b/tests/BenchmarkDotNet.IntegrationTests/AsyncEnumerableParamsSourceTests.cs new file mode 100644 index 0000000000..3a3e9d1d4e --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/AsyncEnumerableParamsSourceTests.cs @@ -0,0 +1,273 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Code; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using BenchmarkDotNet.Toolchains.InProcess.NoEmit; + +namespace BenchmarkDotNet.IntegrationTests; + +// Covers IAsyncEnumerable sources for [ParamsSource] and [ArgumentsSource] (issue #3120). +// Non-constant values (custom reference types) are used on purpose so the smart-parameter code path +// (host discovery + generated async initialization) is exercised rather than embedded literals. +public class AsyncEnumerableParamsSourceTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) +{ + // For [ArgumentsSource] tests. InProcessNoEmitToolchain is intentionally omitted: it doesn't support arguments (see #687). + public static IEnumerable GetToolchains() + { + yield return [InProcessEmitToolchain.Default]; + + if (ContinuousIntegration.IsGitHubDraftPR()) + yield break; + + yield return [Job.Default.GetToolchain()]; + } + + // For [ParamsSource] tests, which all in-process toolchains support. + public static IEnumerable GetParamsToolchains() + { + yield return [InProcessNoEmitToolchain.Default]; + foreach (var toolchain in GetToolchains()) + yield return toolchain; + } + + private Summary Run(Type type, IToolchain toolchain) + { + IConfig config = CreateSimpleConfig(job: Job.Dry.WithToolchain(toolchain)); + if (!toolchain.IsInProcess) + { + // Show the relevant codegen excerpt in test results (the *.notcs is not part of the logs) + Output.WriteLine("// Benchmarks and CodeGenerator.GetParamsContent()"); + BenchmarkRunInfo runInfo = BenchmarkConverter.TypeToBenchmarks(type, config); + foreach (BenchmarkCase benchmarkCase in runInfo.BenchmarksCases) + { + Output.WriteLine("// " + benchmarkCase.DisplayInfo); + Output.WriteLine(CodeGenerator.GetParamsInitializer(benchmarkCase)); + } + } + return CanExecute(type, config); + } + + public class Item + { + public int Data { get; init; } + public override string ToString() => "item" + Data; + } + + public class StaticParamsSource + { + public static async IAsyncEnumerable GetValues() + { + await Task.Yield(); + yield return new Item { Data = 1 }; + await Task.Delay(1); + yield return new Item { Data = 2 }; + yield return new Item { Data = 3 }; + } + + [ParamsSource(nameof(GetValues))] + public Item Target { get; set; } = null!; + + [Benchmark] + public int Benchmark() + => Target.Data > 0 ? Target.Data : throw new InvalidOperationException("Value was not set"); + } + + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void StaticAsyncParamsSource_Succeeds(IToolchain toolchain) => Run(typeof(StaticParamsSource), toolchain); + + public class InstanceParamsSource + { + public async IAsyncEnumerable GetValues() + { + await Task.Yield(); + yield return new Item { Data = 10 }; + yield return new Item { Data = 20 }; + } + + [ParamsSource(nameof(GetValues))] + public Item Target { get; set; } = null!; + + [Benchmark] + public int Benchmark() + => Target.Data > 0 ? Target.Data : throw new InvalidOperationException("Value was not set"); + } + + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void InstanceAsyncParamsSource_Succeeds(IToolchain toolchain) => Run(typeof(InstanceParamsSource), toolchain); + + public class PropertyParamsSource + { + // A property getter can't be an async iterator, so it returns one from an async iterator method. + public static IAsyncEnumerable Values => GetValues(); + + private static async IAsyncEnumerable GetValues() + { + await Task.Yield(); + yield return new Item { Data = 5 }; + yield return new Item { Data = 6 }; + } + + [ParamsSource(nameof(Values))] + public Item Target { get; set; } = null!; + + [Benchmark] + public int Benchmark() + => Target.Data > 0 ? Target.Data : throw new InvalidOperationException("Value was not set"); + } + + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void PropertyAsyncParamsSource_Succeeds(IToolchain toolchain) => Run(typeof(PropertyParamsSource), toolchain); + + // A source method may have all-optional parameters, e.g. an async iterator with an + // [EnumeratorCancellation] CancellationToken; it's invoked with the default (no special handling needed). + public class EnumeratorCancellationParamsSource + { + public static async IAsyncEnumerable GetValues( + [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default) + { + await Task.Yield(); + yield return new Item { Data = 100 }; + await Task.Delay(1, cancellationToken); + yield return new Item { Data = 200 }; + } + + [ParamsSource(nameof(GetValues))] + public Item Target { get; set; } = null!; + + [Benchmark] + public int Benchmark() + => Target.Data > 0 ? Target.Data : throw new InvalidOperationException("Value was not set"); + } + + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void EnumeratorCancellationParamsSource_Succeeds(IToolchain toolchain) => Run(typeof(EnumeratorCancellationParamsSource), toolchain); + + public class SingleArgumentSource + { + public static async IAsyncEnumerable GetArguments() + { + await Task.Yield(); + yield return new Item { Data = 1 }; + await Task.Delay(1); + yield return new Item { Data = 2 }; + } + + [Benchmark] + [ArgumentsSource(nameof(GetArguments))] + public int Benchmark(Item item) + => item.Data > 0 ? item.Data : throw new InvalidOperationException("Argument was not set"); + } + + [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void SingleAsyncArgumentSource_Succeeds(IToolchain toolchain) => Run(typeof(SingleArgumentSource), toolchain); + + public class MultipleArgumentsSource + { + public static async IAsyncEnumerable GetArguments() + { + await Task.Yield(); + yield return [new Item { Data = 1 }, new Item { Data = 10 }]; + await Task.Delay(1); + yield return [new Item { Data = 2 }, new Item { Data = 20 }]; + } + + [Benchmark] + [ArgumentsSource(nameof(GetArguments))] + public int Benchmark(Item first, Item second) + => first.Data + second.Data; + } + + [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void MultipleAsyncArgumentsSource_Succeeds(IToolchain toolchain) => Run(typeof(MultipleArgumentsSource), toolchain); + + // BenchmarkDotNet never installs a SynchronizationContext.Current, so whatever is ambient when a run starts is + // still ambient inside it. Reading a source must not capture it: marshaling belongs to the caller, which applies + // it with ConfigureAwait on the enumerable. The reflection path used for value-type elements handed the + // continuation to the user's raw awaiter, which captured the context and posted to it. +// Both element kinds, because they are read through different machinery: a value-type element goes through + // the reflection loop, a reference-type one through the covariance cast, which had no guard here at all. + [Theory] + [InlineData(typeof(ValueTypeAsyncParamsSource))] + [InlineData(typeof(ReferenceTypeAsyncParamsSource))] + public void ReadingAnAsyncSourceDoesNotPostToTheAmbientSynchronizationContext(Type benchmarkType) + { + var recording = new RecordingSynchronizationContext(); + var original = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(recording); + try + { + Run(benchmarkType, new InProcessEmitToolchain(new() { ExecuteOnSeparateThread = false })); + } + finally + { + SynchronizationContext.SetSynchronizationContext(original); + } + + Assert.Equal(0, recording.PostCount); + Assert.Equal(0, recording.SendCount); + } + + private sealed class RecordingSynchronizationContext : SynchronizationContext + { + private int postCount; + private int sendCount; + + public int PostCount => Volatile.Read(ref postCount); + public int SendCount => Volatile.Read(ref sendCount); + + public override void Post(SendOrPostCallback d, object? state) + { + Interlocked.Increment(ref postCount); + base.Post(d, state); + } + + public override void Send(SendOrPostCallback d, object? state) + { + Interlocked.Increment(ref sendCount); + base.Send(d, state); + } + } + + public class ValueTypeAsyncParamsSource + { + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + // A value-type element takes the reflection path rather than the covariance cast, and the source configures + // its own awaits away so that only BenchmarkDotNet's could reach the ambient context. + public static async IAsyncEnumerable Values() + { + await Task.Delay(1).ConfigureAwait(false); + yield return 1; + await Task.Delay(1).ConfigureAwait(false); + yield return 2; + } + + [Benchmark] + public int Benchmark() => Value; + } + + public class ReferenceTypeAsyncParamsSource + { + [ParamsSource(nameof(Values))] + public string Value { get; set; } = ""; + + // A reference-type element is read through IAsyncEnumerable's covariance rather than by reflection, so + // the awaits are the compiler's own. As above, the source configures its own away, leaving only + // BenchmarkDotNet's able to reach the ambient context. + public static async IAsyncEnumerable Values() + { + await Task.Delay(1).ConfigureAwait(false); + yield return "a"; + await Task.Delay(1).ConfigureAwait(false); + yield return "b"; + } + + [Benchmark] + public string Benchmark() => Value; + } +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/AttributesTests.cs b/tests/BenchmarkDotNet.IntegrationTests/AttributesTests.cs index 8130a7ce73..72fa11d45a 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/AttributesTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/AttributesTests.cs @@ -20,7 +20,7 @@ public class ConsumingCustomAttributes [CustomParams(ExpectedNumber)] public int Number; - public required string Text; + public string Text = ""; [CustomGlobalSetup] public void Setup() diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj index b296f3ac1a..5907c3bcef 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkDotNet.IntegrationTests.csproj @@ -1,4 +1,4 @@ - + BenchmarkDotNet.IntegrationTests diff --git a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkSwitcherTest.cs b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkSwitcherTest.cs index 9f5fcc60a2..ff0fd75f22 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/BenchmarkSwitcherTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/BenchmarkSwitcherTest.cs @@ -355,7 +355,11 @@ private class UserInteractionMock : IUserInteraction public void PrintNoBenchmarksError(ILogger logger) => PrintNoBenchmarksErrorCalledTimes++; - public void PrintWrongFilterInfo(IReadOnlyList allTypes, ILogger logger, string[] userFilters) => PrintWrongFilterInfoCalledTimes++; + public ValueTask PrintWrongFilterInfoAsync(IReadOnlyList allTypes, ILogger logger, string[] userFilters, CancellationToken cancellationToken) + { + PrintWrongFilterInfoCalledTimes++; + return default; + } public IReadOnlyList AskUser(IReadOnlyList allTypes, ILogger logger) { diff --git a/tests/BenchmarkDotNet.IntegrationTests/CancellationTokenTests.cs b/tests/BenchmarkDotNet.IntegrationTests/CancellationTokenTests.cs index 8e37f112dd..2de029c669 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/CancellationTokenTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/CancellationTokenTests.cs @@ -11,6 +11,7 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Tests.Loggers; using BenchmarkDotNet.Tests.XUnit; +using BenchmarkDotNet.Toolchains; using BenchmarkDotNet.Toolchains.DotNetCli; using BenchmarkDotNet.Toolchains.InProcess.Emit; using BenchmarkDotNet.Toolchains.InProcess.NoEmit; @@ -32,6 +33,36 @@ public void BenchmarkWithCancellationTokenProperty_ReceivesToken() CanExecute(config); } + // GetFields hands back a hidden base field alongside the `new` one hiding it, and the token is assigned + // through an object initializer, where a repeated name is CS1912 - the generated code failed to build. + // (GetProperties collapses the pair, so only fields reach this.) + [Fact] + public void BenchmarkHidingAnInheritedCancellationTokenField_BuildsAndReceivesToken() + { + var config = ManualConfig.CreateEmpty() + .AddJob(Job.Dry) + .AddLogger(new OutputLogger(Output)); + + CanExecute(config); + } + + public class BenchmarkHidingCancellationTokenFieldBase + { + [BenchmarkCancellation] public CancellationToken Token; + } + + public class BenchmarkHidingCancellationTokenField : BenchmarkHidingCancellationTokenFieldBase + { + [BenchmarkCancellation] public new CancellationToken Token; + + [Benchmark] + public void CheckToken() + { + Assert.True(Token.CanBeCanceled); + Assert.False(Token.IsCancellationRequested); + } + } + [Fact] public void BenchmarkWithCancellationTokenProperty_ReceivesToken_InProcessNoEmit() { @@ -52,6 +83,28 @@ public void BenchmarkWithCancellationTokenProperty_ReceivesToken_InProcessEmit() CanExecute(config); } + [Theory] + [MemberData(nameof(CancellationToolchains), DisableDiscoveryEnumeration = true)] + public void StaticCancellationTokenOnABaseTypeReceivesToken(IToolchain toolchain) + { + var config = ManualConfig.CreateEmpty() + .AddJob(Job.Dry.WithToolchain(toolchain)) + .AddLogger(new OutputLogger(Output)); + + CanExecute(config); + } + + public static IEnumerable CancellationToolchains() + { + yield return [InProcessNoEmitToolchain.Default]; + yield return [InProcessEmitToolchain.Default]; + + if (ContinuousIntegration.IsGitHubDraftPR()) + yield break; + + yield return [Job.Default.GetToolchain()]; + } + [TheoryEnvSpecific("JSVU does not support ARM on Windows or Linux", EnvRequirement.NonWindowsArm, EnvRequirement.NonLinuxArm, EnvRequirement.NonGitHubDraftPR)] [InlineData("v8")] [InlineData("node")] @@ -153,6 +206,25 @@ public void CheckToken() } } + // A static [BenchmarkCancellation] member declared on a base type. Reflection withholds a base type's statics + // unless FlattenHierarchy is asked for, which BenchmarkCancellationValidator asks for and the assignment sites + // did not - so the member validated but was never written, leaving the benchmark a default token. + public class StaticCancellationTokenOnABase + { + [BenchmarkCancellation] + public static CancellationToken InheritedToken { get; set; } + } + + public class InheritsStaticCancellationToken : StaticCancellationTokenOnABase + { + [Benchmark] + public void CheckToken() + { + Assert.True(InheritedToken.CanBeCanceled); + Assert.False(InheritedToken.IsCancellationRequested); + } + } + public class SimpleBenchmark { [Benchmark] diff --git a/tests/BenchmarkDotNet.IntegrationTests/ConflictingNamesTests.cs b/tests/BenchmarkDotNet.IntegrationTests/ConflictingNamesTests.cs index bb1b8eab7e..aac899053d 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/ConflictingNamesTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/ConflictingNamesTests.cs @@ -4,6 +4,10 @@ namespace BenchmarkDotNet.IntegrationTests; public class ConflictingNamesTests(ITestOutputHelper output) : BenchmarkTestExecutor(output) { + // The un-prefixed template names (OverheadActionUnroll, WorkloadActionUnroll, ...) are usable because generated + // members are __-prefixed, and a benchmark method named like a generated member (e.g. __Overhead) coexists with it + // via C# member hiding. Only a [Params*] member's name can collide (it's assigned by the runnable's object + // initializer) - that case is guarded by the ParamsValidator / BDN1208 analyzer instead. #2821 [Fact] public void BenchmarkMethodsCanUseTemplateNames() => CanExecute(); @@ -12,6 +16,13 @@ public class WithNamesUsedByTemplate [Params(1)] public int OverheadActionUnroll { get; set; } + // The runnable's entry point and arguments struct are __Run and __FieldsContainer, so these names are free. + [Params(2)] + public int Run { get; set; } + + [Params(3)] + public int FieldsContainer { get; set; } + [Benchmark] [Arguments(2)] public void System(int OverheadActionNoUnroll) @@ -25,6 +36,20 @@ public void BenchmarkDotNet() } + // Not a compile-time constant, so the child process re-obtains it through an expression the renderer emits + // - the only place the generated code names the BenchmarkDotNet namespace from inside this type. + public static IEnumerable NonConstantValues() + { + yield return new object(); + } + + [Benchmark] + [ArgumentsSource(nameof(NonConstantValues))] + public void NonConstantArgument(object argument) + { + + } + [Benchmark] public void __Overhead() { @@ -38,4 +63,4 @@ public void WorkloadActionUnroll(int WorkloadActionNoUnroll) } } -} \ No newline at end of file +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs index 8cea409606..6dcc114929 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/NaiveRunnableEmitDiff.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Attributes.CompilerServices; +using BenchmarkDotNet.Code; using BenchmarkDotNet.Loggers; using Mono.Cecil; using Mono.Cecil.Cil; @@ -14,9 +15,7 @@ public class NaiveRunnableEmitDiff "BenchmarkDotNet.Autogenerated.UniqueProgramName", "BenchmarkDotNet.Autogenerated.DirtyAssemblyResolveHelper", // not required to be used in the InProcess toolchains (it's already used in the host process) // Poly-filled types added in old runtimes. - "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute", - "System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute", - "System.Runtime.CompilerServices.RequiredMemberAttribute" + "System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute" ]; private static readonly HashSet IgnoredAttributeTypeNames = @@ -26,9 +25,8 @@ public class NaiveRunnableEmitDiff private static readonly HashSet IgnoredRunnableMethodNames = [ - "Run", - ".ctor", - "__ResolveWorkloadMethods" + RunnableConstants.RunMethodName, + ".ctor" ]; private static readonly IReadOnlyDictionary AltOpCodes = new Dictionary() @@ -63,7 +61,7 @@ public static void RunDiff(string roslynAssemblyPath, string emittedAssemblyPath } private static bool IsRunnable(TypeReference t) => - t.FullName.StartsWith("BenchmarkDotNet.Autogenerated.Runnable_"); + t.FullName.StartsWith(RunnableConstants.EmittedTypePrefix); private static bool AreSameTypeIgnoreNested(TypeReference left, TypeReference right) { diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/RunnableRefArgsFromSourceBenchmark.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/RunnableRefArgsFromSourceBenchmark.cs new file mode 100644 index 0000000000..7ae7649b72 --- /dev/null +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcess.EmitTests/RunnableRefArgsFromSourceBenchmark.cs @@ -0,0 +1,39 @@ +using BenchmarkDotNet.Attributes; +// ReSharper disable UnusedMember.Global + +namespace BenchmarkDotNet.IntegrationTests.InProcess.EmitTests; + +// A source's values are rendered inline when SourceCodeHelper can embed them, exactly as [Arguments] values are, +// so a `ref` parameter only reaches the field the extractor fills when its value is a type it cannot - RefArgBox +// here. The int and string beside it stay constants, so one call mixes both forms. +public readonly struct RefArgBox(double value) +{ + public double Value { get; } = value; +} + +public class RunnableRefArgsFromSourceBenchmark +{ + public static IEnumerable ManyArgs() + { + yield return new object[] { new RefArgBox(123.0), 4, "5" }; + } + + public static IEnumerable SingleArg() + { + yield return new RefArgBox(123.0); + } + + private int refResultHolder; + + [Benchmark, ArgumentsSource(nameof(SingleArg))] + public double RefArgFromSourceCase(ref RefArgBox arg0) => arg0.Value; + + // No `in` case: the compiler puts [IsReadOnly] on an `in` parameter and RunnableEmitter does not, so the + // two runnables differ on any benchmark taking one, from [Arguments] as readily as from a source. + + [Benchmark, ArgumentsSource(nameof(ManyArgs))] + public double ManyRefArgsFromSourceCase(ref RefArgBox arg0, int arg1, string arg2) => arg0.Value; + + [Benchmark, ArgumentsSource(nameof(ManyArgs))] + public ref int RefReturnRefArgFromSourceCase(ref RefArgBox arg0, int arg1, string arg2) => ref refResultHolder; +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs index 7c6b7bdfb8..8d64f2bce1 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs @@ -111,11 +111,16 @@ public void InProcessBenchmarkSimpleCasesReflectionEmitSupported() [InlineData(typeof(RunnableStructCaseBenchmark), false)] [InlineData(typeof(RunnableClassCaseBenchmark), false)] [InlineData(typeof(RunnableManyArgsCaseBenchmark), false)] + [InlineData(typeof(RunnableRefArgsFromSourceBenchmark), false)] // ref parameters fed from [ArgumentsSource] [InlineData(typeof(RunnableTaskCaseBenchmark), false)] [InlineData(typeof(RunnableTaskCaseBenchmark), true)] [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableBenchmarks), false)] [InlineData(typeof(AsyncEnumerableBenchmarksTests.AsyncEnumerableCallerOverride), false)] [InlineData(typeof(AsyncEnumerableBenchmarksTests.CustomAsyncEnumerableBenchmarks), false)] + [InlineData(typeof(AsyncEnumerableParamsSourceTests.StaticParamsSource), false)] // async IAsyncEnumerable [ParamsSource] + // A void, parameterless workload with an ASYNC setup/cleanup: the only case that runs the sync core + // emitter's setup state machines. RunnableTaskCaseBenchmark covers the Task-returning emitter's. + [InlineData(typeof(GlobalSetupCleanupTask), false)] public void InProcessBenchmarkEmitsSameIL(Type benchmarkType, bool consumeTasksSynchronously) { var logger = new OutputLogger(Output); diff --git a/tests/BenchmarkDotNet.IntegrationTests/ParamSourceTests.cs b/tests/BenchmarkDotNet.IntegrationTests/ParamSourceTests.cs index 4aaacbd623..7a9dde5550 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/ParamSourceTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/ParamSourceTests.cs @@ -6,6 +6,7 @@ using BenchmarkDotNet.Running; using BenchmarkDotNet.Toolchains; using BenchmarkDotNet.Toolchains.InProcess.Emit; +using BenchmarkDotNet.Toolchains.InProcess.NoEmit; namespace BenchmarkDotNet.IntegrationTests { @@ -23,12 +24,122 @@ public static IEnumerable GetToolchains() yield return [Job.Default.GetToolchain()]; } + // InProcessNoEmit doesn't support arguments (#687), so only parameter tests can use it. + public static IEnumerable GetParamsToolchains() + { + yield return [InProcessNoEmitToolchain.Default]; + foreach (var toolchain in GetToolchains()) + yield return toolchain; + } + [Fact] public void ParamSourceCanHandleStringWithSurrogates() { CanExecute(CreateSimpleConfig()); } + // Not a compilation-time constant, so the generated code re-obtains it from the source rather than + // embedding a literal - which is the path these tests are about. + public class Box + { + public int Value { get; set; } + } + + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void StaticParamCanUseInstanceSource(IToolchain toolchain) + => CanExecuteWithExtraInfo(typeof(StaticParamFromInstanceSource), toolchain); + + public class StaticParamFromInstanceSource + { + public IEnumerable Values() + { + yield return new Box { Value = 42 }; + } + + [ParamsSource(nameof(Values))] + public static Box Value { get; set; } = null!; + + [Benchmark] + public int Run() + => Value.Value == 42 ? Value.Value : throw new InvalidOperationException($"Wrong parameter: {Value.Value}."); + } + + // The runnable assigns parameters through an object initializer, which can set an init-only setter; the + // in-process toolchains reach the same setter reflectively. + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void InitOnlyParamIsSupported(IToolchain toolchain) + => CanExecuteWithExtraInfo(typeof(InitOnlyParam), toolchain); + + public class InitOnlyParam + { + [Params(42)] + public int Value { get; init; } + + [Benchmark] + public int Run() + => Value == 42 ? Value : throw new InvalidOperationException($"The init-only parameter was not set (Value = {Value})."); + } + + public interface IBox + { + int Value { get; } + } + + public class BoxImpl : IBox + { + public int Value { get; set; } + } + + // The counterpart shape: the same single-argument benchmark fed by a source that yields the argument + // itself rather than a one-element argument list. Both are supported - the generated code indexes where + // the source is declared to yield object[], which this one is not. + [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void SingleArgumentSourceYieldingTheValueDirectlyIsNotIndexed(IToolchain toolchain) + => CanExecuteWithExtraInfo(typeof(SingleBaseTypedArgumentFromObject), toolchain); + + public class SingleBaseTypedArgumentFromObject + { + public IEnumerable Data() + { + yield return new BoxImpl { Value = 7 }; + } + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public int Run(IBox box) + => box.Value == 7 ? box.Value : throw new InvalidOperationException($"Wrong argument: {box}."); + } + + // One read serves the whole row, so a sequence that can only be enumerated once is enough - and before + // the row became the unit, each argument got its own invocation and this guard could never fire. + [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + public void InstanceArgumentsSourceIsInvokedOncePerCase(IToolchain toolchain) + => CanExecuteWithExtraInfo(typeof(InstanceSingleEnumerationArgumentsSource), toolchain); + + public class InstanceSingleEnumerationArgumentsSource + { + public IEnumerable Data() => new SingleEnumeration(); + + [Benchmark] + [ArgumentsSource(nameof(Data))] + public int Sum(Box a, Box b) => a.Value + b.Value; + + private sealed class SingleEnumeration : IEnumerable + { + private bool enumerated; + + public IEnumerator GetEnumerator() + { + if (enumerated) + throw new InvalidOperationException("The source sequence was enumerated more than once."); + enumerated = true; + yield return new object[] { new Box { Value = 1 }, new Box { Value = 2 } }; + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } + } + public class ParamSourceIsStringWithSurrogates { public IEnumerable StringValues @@ -60,7 +171,7 @@ private Summary CanExecuteWithExtraInfo(Type type, IToolchain toolchain) foreach (BenchmarkCase benchmarkCase in runInfo.BenchmarksCases) { Output.WriteLine("// " + benchmarkCase.DisplayInfo); - Output.WriteLine(CodeGenerator.GetParamsContent(benchmarkCase)); + Output.WriteLine(CodeGenerator.GetParamsInitializer(benchmarkCase)); } } return CanExecute(type, config); @@ -94,7 +205,7 @@ public class PrivateClassWithPublicInterface public int Benchmark() => ParamsTarget?.Data ?? 0; } - [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] public void PrivateClassWithPublicInterface_Succeeds(IToolchain toolchain) => CanExecuteWithExtraInfo(typeof(PrivateClassWithPublicInterface), toolchain); public class PrivateClassWithPublicInterface_Array @@ -114,7 +225,7 @@ public class PrivateClassWithPublicInterface_Array public int Benchmark() => ParamsTarget?.Sum(p => p?.Data ?? 0) ?? 0; } - [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] public void PrivateClassWithPublicInterface_Array_Succeeds(IToolchain toolchain) => CanExecuteWithExtraInfo(typeof(PrivateClassWithPublicInterface_Array), toolchain); public class PrivateClassWithPublicInterface_Enumerable @@ -135,7 +246,7 @@ public class PrivateClassWithPublicInterface_Enumerable public int Benchmark() => ParamsTarget?.Sum(p => p?.Data ?? 0) ?? 0; } - [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] public void PrivateClassWithPublicInterface_Enumerable_Succeeds(IToolchain toolchain) => CanExecuteWithExtraInfo(typeof(PrivateClassWithPublicInterface_Enumerable), toolchain); public class PrivateClassWithPublicInterface_AsObject @@ -154,7 +265,7 @@ public class PrivateClassWithPublicInterface_AsObject public int Benchmark() => ParamsTarget?.Data ?? 0; } - [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] public void PrivateClassWithPublicInterface_AsObject_Succeeds(IToolchain toolchain) => CanExecuteWithExtraInfo(typeof(PrivateClassWithPublicInterface_AsObject), toolchain); public class PublicSource @@ -219,7 +330,7 @@ public class OverrideProperty : OverridePropertyBase public int Benchmark() => ParamsTarget; } - [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] public void OverrideProperty_Succeeds(IToolchain toolchain) => CanExecuteWithExtraInfo(typeof(OverrideProperty), toolchain); public abstract class OverrideMethodBase @@ -238,7 +349,119 @@ public class OverrideMethod : OverrideMethodBase public int Benchmark() => ParamsTarget; } - [Theory, MemberData(nameof(GetToolchains), DisableDiscoveryEnumeration = true)] + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] public void OverrideMethod_Succeeds(IToolchain toolchain) => CanExecuteWithExtraInfo(typeof(OverrideMethod), toolchain); + + public class StaticParamsOnABase + { + [Params(1, 2)] + public static int InheritedField; + + [ParamsSource(nameof(Values))] + public static int InheritedFromSource { get; set; } + + public static IEnumerable Values() { yield return 7; } + } + + public class InheritsStaticParams : StaticParamsOnABase + { + // Asserted here rather than on the summary's standard output, which the in-process toolchains do not fill. + [Benchmark] + public int Benchmark() + => InheritedFromSource == 7 && InheritedField is 1 or 2 + ? InheritedField + : throw new InvalidOperationException($"Expected 1|2 from 7, got {InheritedField} from {InheritedFromSource}."); + } + + // Reflection withholds a base type's statics unless FlattenHierarchy is asked for, so binding these at all + // depends on discovery asking for it - and every toolchain has to reach them the same way afterwards: the + // generated code through the benchmark type's name, the in-process ones by looking the member up again. + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void ParamsOnABaseTypeStaticMemberAreAssigned(IToolchain toolchain) + => CanExecuteWithExtraInfo(typeof(InheritsStaticParams), toolchain); + + public class HidesBaseMembers + { + protected int Hidden { get; set; } + + public static int HiddenStatic { get; set; } + + public string HiddenField = ""; + + // Same name and same type as the member hiding it, so matching on the declared type leaves two + // candidates and only the most derived one is the parameter. + public static string HiddenStaticField = ""; + + // Hidden by a member of the other kind: looking properties up before fields finds these instead of + // the fields that carry the attribute. + public string PropertyHiddenByField { get; set; } = ""; + + public static string StaticPropertyHiddenByField { get; set; } = ""; + } + + // Looking a parameter up by name alone finds both the member and the one it hides, which reflection reports + // as an ambiguous name rather than a choice. + public class HidingParams : HidesBaseMembers + { + [Params("a")] + public new string Hidden { get; set; } = null!; + + [Params("b")] + public static new string HiddenStatic { get; set; } = null!; + + [Params("c")] + public new string HiddenField = null!; + + [Params("d")] + public static new string HiddenStaticField = null!; + + [Params("e")] + public new string PropertyHiddenByField = null!; + + [Params("f")] + public static new string StaticPropertyHiddenByField = null!; + + [Benchmark] + public string Benchmark() + => Hidden == "a" && HiddenStatic == "b" && HiddenField == "c" && HiddenStaticField == "d" + && PropertyHiddenByField == "e" && StaticPropertyHiddenByField == "f" + ? Hidden + HiddenStatic + HiddenField + HiddenStaticField + PropertyHiddenByField + StaticPropertyHiddenByField + : throw new InvalidOperationException( + $"Expected a/b/c/d/e/f, got {Hidden}/{HiddenStatic}/{HiddenField}/{HiddenStaticField}" + + $"/{PropertyHiddenByField}/{StaticPropertyHiddenByField}."); + } + + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void ParamsHidingABaseMemberAreAssigned(IToolchain toolchain) + => CanExecuteWithExtraInfo(typeof(HidingParams), toolchain); + + // The members above hide ones that carry no attribute, so only one declaration is ever a parameter. + // Here both carry it, which is what makes the pair reach the parameter list: GetFields reports a hidden + // base field alongside the `new` one hiding it, where GetProperties collapses the pair. Two parameters of + // one name multiply the cases against each other and emit the name twice in the runnable's object + // initializer (CS1912), so only the most derived declaration may survive. + public class AttributedOnBothBase + { + [Params(1, 2)] public int SharedField; + + [Params(3, 4)] public int SharedProperty { get; set; } + } + + public class ParamsOnBothDeclarations : AttributedOnBothBase + { + [Params(5)] public new int SharedField; + + [Params(6)] public new int SharedProperty { get; set; } + + [Benchmark] + public int Benchmark() + => SharedField == 5 && SharedProperty == 6 + ? SharedField + : throw new InvalidOperationException($"Expected 5/6, got {SharedField}/{SharedProperty}."); + } + + [Theory, MemberData(nameof(GetParamsToolchains), DisableDiscoveryEnumeration = true)] + public void ParamsOnBothADerivedMemberAndTheOneItHidesUseTheDerivedOne(IToolchain toolchain) + => CanExecuteWithExtraInfo(typeof(ParamsOnBothDeclarations), toolchain); } } diff --git a/tests/BenchmarkDotNet.IntegrationTests/ParamsTests.cs b/tests/BenchmarkDotNet.IntegrationTests/ParamsTests.cs index 5db376879d..4d9130a057 100644 --- a/tests/BenchmarkDotNet.IntegrationTests/ParamsTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/ParamsTests.cs @@ -238,4 +238,4 @@ public class ParamsTestRequiredProperty } #endif } -} \ No newline at end of file +} diff --git a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj index 0a9cb76c9b..2b95d20bae 100755 --- a/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj +++ b/tests/BenchmarkDotNet.Tests/BenchmarkDotNet.Tests.csproj @@ -1,4 +1,4 @@ - + BenchmarkDotNet.Tests diff --git a/tests/BenchmarkDotNet.Tests/Columns/MetricColumnTests.cs b/tests/BenchmarkDotNet.Tests/Columns/MetricColumnTests.cs index 62fb80b21c..08650984ee 100644 --- a/tests/BenchmarkDotNet.Tests/Columns/MetricColumnTests.cs +++ b/tests/BenchmarkDotNet.Tests/Columns/MetricColumnTests.cs @@ -35,7 +35,7 @@ private static Summary CreateMockSummary(bool printUnitsInContent, bool printUni var benchmarkCase = new BenchmarkCase( new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, - new ParameterInstances([]), + ParameterInstances.Empty, ImmutableConfigBuilder.Create(config)); var metric = new Metric(LocalMetricDescriptor.TimeInstance, metricValue); var benchmarkReport = new BenchmarkReport(true, benchmarkCase, null!, null!, null, [metric]); diff --git a/tests/BenchmarkDotNet.Tests/Exporters/OpenMetricsExporterTests.cs b/tests/BenchmarkDotNet.Tests/Exporters/OpenMetricsExporterTests.cs index 94fdb4723b..a61ca83852 100644 --- a/tests/BenchmarkDotNet.Tests/Exporters/OpenMetricsExporterTests.cs +++ b/tests/BenchmarkDotNet.Tests/Exporters/OpenMetricsExporterTests.cs @@ -33,7 +33,7 @@ public async Task SingleBenchmark_ProducesHelpAndTypeOnce() new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, new ParameterInstances([ - new ParameterInstance(new ParameterDefinition("param1", false, ["Parameter 1"], true, typeof(string), 0 ), "value1", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param1", isStatic: false, isArgument: true, typeof(string), 0), "value1", SummaryStyle.Default), ]), ImmutableConfigBuilder.Create(new ManualConfig())), null!, @@ -52,7 +52,7 @@ public async Task SingleBenchmark_ProducesHelpAndTypeOnce() new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, new ParameterInstances([ - new ParameterInstance(new ParameterDefinition("param1", false, ["Parameter 1"], true, typeof(string), 0 ), "value2", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param1", isStatic: false, isArgument: true, typeof(string), 0), "value2", SummaryStyle.Default), ]), ImmutableConfigBuilder.Create(new ManualConfig())), null!, @@ -71,7 +71,7 @@ public async Task SingleBenchmark_ProducesHelpAndTypeOnce() new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, new ParameterInstances([ - new ParameterInstance(new ParameterDefinition("param1", false, ["Parameter 1"], true, typeof(string), 0 ), "value3", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param1", isStatic: false, isArgument: true, typeof(string), 0), "value3", SummaryStyle.Default), ]), ImmutableConfigBuilder.Create(new ManualConfig())), null!, @@ -113,9 +113,9 @@ public async Task ParametrizedBenchmarks_LabelExpansion() new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, new ParameterInstances([ - new ParameterInstance(new ParameterDefinition("param1", false, ["Parameter 1"], true, typeof(string), 0 ), "value1", SummaryStyle.Default), - new ParameterInstance(new ParameterDefinition("param2", false, ["Parameter 2"], true, typeof(string), 0 ), "value1", SummaryStyle.Default), - new ParameterInstance(new ParameterDefinition("param3", false, ["Parameter 3"], true, typeof(string), 0 ), "value1", SummaryStyle.Default) + new ParameterInstance(new ParameterDefinition("param1", isStatic: false, isArgument: true, typeof(string), 0), "value1", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param2", isStatic: false, isArgument: true, typeof(string), 0), "value1", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param3", isStatic: false, isArgument: true, typeof(string), 0), "value1", SummaryStyle.Default) ]), ImmutableConfigBuilder.Create(new ManualConfig())), null!, @@ -134,9 +134,9 @@ public async Task ParametrizedBenchmarks_LabelExpansion() new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, new ParameterInstances([ - new ParameterInstance(new ParameterDefinition("param1", false, ["Parameter 1"], true, typeof(string), 0 ), "value2", SummaryStyle.Default), - new ParameterInstance(new ParameterDefinition("param2", false, ["Parameter 2"], true, typeof(string), 0 ), "value2", SummaryStyle.Default), - new ParameterInstance(new ParameterDefinition("param3", false, ["Parameter 3"], true, typeof(string), 0 ), "value2", SummaryStyle.Default) ]), + new ParameterInstance(new ParameterDefinition("param1", isStatic: false, isArgument: true, typeof(string), 0), "value2", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param2", isStatic: false, isArgument: true, typeof(string), 0), "value2", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param3", isStatic: false, isArgument: true, typeof(string), 0), "value2", SummaryStyle.Default) ]), ImmutableConfigBuilder.Create(new ManualConfig())), null!, null!, @@ -154,9 +154,9 @@ public async Task ParametrizedBenchmarks_LabelExpansion() new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, new ParameterInstances([ - new ParameterInstance(new ParameterDefinition("param1", false, ["Parameter 1"], true, typeof(string), 0 ), "value3", SummaryStyle.Default), - new ParameterInstance(new ParameterDefinition("param2", false, ["Parameter 2"], true, typeof(string), 0 ), "value3", SummaryStyle.Default), - new ParameterInstance(new ParameterDefinition("param3", false, ["Parameter 3"], true, typeof(string), 0 ), "value3", SummaryStyle.Default) ]), + new ParameterInstance(new ParameterDefinition("param1", isStatic: false, isArgument: true, typeof(string), 0), "value3", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param2", isStatic: false, isArgument: true, typeof(string), 0), "value3", SummaryStyle.Default), + new ParameterInstance(new ParameterDefinition("param3", isStatic: false, isArgument: true, typeof(string), 0), "value3", SummaryStyle.Default) ]), ImmutableConfigBuilder.Create(new ManualConfig())), null!, null!, @@ -195,7 +195,7 @@ public async Task LabelsAreEscapedCorrectly() new BenchmarkCase( new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, - new ParameterInstances([]), + ParameterInstances.Empty, ImmutableConfigBuilder.Create(new ManualConfig())), null!, null!, @@ -245,7 +245,7 @@ public async Task DecimalSeparator_UsesInvariantCulture() benchmarkCase: new BenchmarkCase( new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, - new ParameterInstances([]), + ParameterInstances.Empty, ImmutableConfigBuilder.Create(new ManualConfig())), null!, null!, @@ -293,7 +293,7 @@ public async Task MemoryDiagnoser_ExportsAllocatedBytesPerOperation() benchmarkCase: new BenchmarkCase( new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, - new ParameterInstances([]), + ParameterInstances.Empty, ImmutableConfigBuilder.Create(config)), null!, null!, @@ -337,7 +337,7 @@ public async Task MemoryDiagnoser_WithoutAllocationData_ExportsAllocatedBytesAsN benchmarkCase: new BenchmarkCase( new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, - new ParameterInstances([]), + ParameterInstances.Empty, ImmutableConfigBuilder.Create(config)), null!, null!, @@ -380,7 +380,7 @@ public async Task WithoutMemoryDiagnoser_CustomAllocatedBytesMetricIsNotSuppress benchmarkCase: new BenchmarkCase( new Descriptor(MockFactory.MockType, MockFactory.MockMethodInfo), Job.Dry, - new ParameterInstances([]), + ParameterInstances.Empty, ImmutableConfigBuilder.Create(new ManualConfig())), null!, null!, diff --git a/tests/BenchmarkDotNet.Tests/Helpers/DynamicAwaitHelperTests.cs b/tests/BenchmarkDotNet.Tests/Helpers/DynamicAwaitHelperTests.cs new file mode 100644 index 0000000000..2eb5157047 --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Helpers/DynamicAwaitHelperTests.cs @@ -0,0 +1,304 @@ +using BenchmarkDotNet.Extensions; +using BenchmarkDotNet.Helpers; +using System.Runtime.CompilerServices; + +namespace BenchmarkDotNet.Tests.Helpers; + +public class DynamicAwaitHelperTests +{ + + // The caller's cancellation token must reach the source's [EnumeratorCancellation] for both element kinds: + // reference types go through the covariance cast, value types through the reflection loop (+ GetAsyncEnumeratorArgs). + // Only the source path takes a token at all - EnumerateBenchmarkAsync deliberately has none, because real + // execution never forces the ambient token onto a benchmark's own sequence. + [Theory] + [InlineData(typeof(IAsyncEnumerable))] // value-type element -> reflection loop + [InlineData(typeof(IAsyncEnumerable))] // reference-type element -> covariance cast + public async Task EnumerateSourceAsync_ForwardsCancellationTokenToSource(Type asyncEnumerableType) + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + object source = asyncEnumerableType == typeof(IAsyncEnumerable) + ? ValueTypeSource() + : ReferenceTypeSource(); + + var enumerable = DynamicAwaitHelper.EnumerateSourceAsync(source, asyncEnumerableType.GetGenericArguments()[0]); + + var exception = await Record.ExceptionAsync(async () => + { + await foreach (var _ in enumerable.WithCancellation(cts.Token)) + { + } + }); + + // Both paths surface the OperationCanceledException itself. The covariance path never reflects, and the + // reflection loop routes every reflective call through DynamicAwaitHelper.Unwrapped - GetResult included, + // which is where an awaited MoveNextAsync throws - so the TargetInvocationException is already gone. + // Asserted directly rather than through an InnerException fallback, which could never fire and would have + // let a lost unwrap pass. + Assert.NotNull(exception); + var operationCanceled = Assert.IsAssignableFrom(exception); + Assert.Equal(cts.Token, operationCanceled.CancellationToken); + } + + // A type can implement IAsyncEnumerable and still bind await-foreach to its own pattern method. Draining it + // must follow the pattern - what the benchmark itself enumerates - not the interface. + [Fact] + public async Task EnumerateAsync_PrefersThePatternOverTheImplementedInterface() + { + // The declared type is the interface: that is where the pattern and IAsyncEnumerable both apply. + Assert.True(typeof(IPatternAndInterfaceSource).IsAsyncEnumerable(out var info)); + + var items = new List(); + await foreach (var item in DynamicAwaitHelper.EnumerateBenchmarkAsync(new PatternAndInterfaceSource(), info!)) + { + items.Add(item); + } + + Assert.Equal(new object?[] { "pattern" }, items); + } + + // An optional parameter can lack a declared default ([Optional] with no [DefaultParameterValue]). Guards the + // argument defaulting: Type.Missing would be rejected by Invoke, unlike the null this passes. + [Fact] + public async Task EnumerateAsync_HandlesOptionalParametersWithoutADeclaredDefault() + { + Assert.True(typeof(OptionalWithoutDefaultSource).IsAsyncEnumerable(out var info)); + + var items = new List(); + await foreach (var item in DynamicAwaitHelper.EnumerateBenchmarkAsync(new OptionalWithoutDefaultSource(), info!)) + { + items.Add(item); + } + + Assert.Equal(new object?[] { 0 }, items); + } + + private sealed class OptionalWithoutDefaultSource + { + // Value-type element, so enumeration goes through the reflection loop and its argument defaulting. + public OptionalEnumerator GetAsyncEnumerator([System.Runtime.InteropServices.Optional] CancellationToken cancellationToken) => new(); + } + + private sealed class OptionalEnumerator + { + private bool moved; + public int Current => 0; + + public ValueTask MoveNextAsync([System.Runtime.InteropServices.Optional] bool unused) + { + bool hasMore = !moved; + moved = true; + return new ValueTask(hasMore); + } + } + + private interface IPatternAndInterfaceSource : IAsyncEnumerable + { + // await-foreach binds to this in preference to the inherited IAsyncEnumerable member. + new PatternEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default); + } + + private sealed class PatternAndInterfaceSource : IPatternAndInterfaceSource + { + public PatternEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new(); + + IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) + => new InterfaceEnumerator(); + } + + private sealed class PatternEnumerator + { + private bool moved; + public string Current => "pattern"; + + public ValueTask MoveNextAsync() + { + bool hasMore = !moved; + moved = true; + return new ValueTask(hasMore); + } + } + + private sealed class InterfaceEnumerator : IAsyncEnumerator + { + private bool moved; + public string Current => "interface"; + + public ValueTask MoveNextAsync() + { + bool hasMore = !moved; + moved = true; + return new ValueTask(hasMore); + } + + public ValueTask DisposeAsync() => default; + } + + // The element-type overload binds IAsyncEnumerable, so it must reach the implementation however the source + // declares it - and must not be diverted by an await-foreach pattern method the source also happens to declare. + [Theory] + [InlineData(typeof(ImplicitValueSource), 1)] + [InlineData(typeof(ExplicitValueSource), 2)] + [InlineData(typeof(PatternAndInterfaceValueSource), 3)] + [InlineData(typeof(BrokenPatternAndInterfaceValueSource), 4)] + public async Task EnumerateAsync_BindsTheInterface_ForEveryImplementationShape(Type sourceType, int expected) + { + object source = Activator.CreateInstance(sourceType)!; + + // Discovery's gate: the interface check is what routes here. + Assert.True(sourceType.IsIAsyncEnumerable(out var elementType)); + Assert.Equal(typeof(int), elementType); + + List items = []; + await foreach (var item in DynamicAwaitHelper.EnumerateSourceAsync(source, elementType!)) + { + items.Add(item); + } + + Assert.Equal([expected], items); + } + + private sealed class ImplicitValueSource : IAsyncEnumerable + { + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + => Yield(1).GetAsyncEnumerator(cancellationToken); + } + + private sealed class ExplicitValueSource : IAsyncEnumerable + { + IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) + => Yield(2).GetAsyncEnumerator(cancellationToken); + } + + // Declares a conforming await-foreach pattern method over a different element type, explicitly implementing the + // interface. Binding the pattern would enumerate strings; the interface is the source contract, so it wins. + private sealed class PatternAndInterfaceValueSource : IAsyncEnumerable + { + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + => Yield("pattern").GetAsyncEnumerator(cancellationToken); + + IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) + => Yield(3).GetAsyncEnumerator(cancellationToken); + } + + // Declares a pattern method whose return type is not an enumerator at all. IsAsyncEnumerable commits to the + // pattern and rejects such a type outright, so resolving through it would throw on a usable source. + private sealed class BrokenPatternAndInterfaceValueSource : IAsyncEnumerable + { + public int GetAsyncEnumerator(CancellationToken cancellationToken = default) => 0; + + IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) + => Yield(4).GetAsyncEnumerator(cancellationToken); + } + + private static async IAsyncEnumerable Yield(T value) + { + await Task.Yield(); + yield return value; + } + + private static async IAsyncEnumerable ValueTypeSource([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return 1; + } + + private static async IAsyncEnumerable ReferenceTypeSource([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return "value"; + } + + // ICriticalNotifyCompletion is optional in the awaiter pattern. DynamicAwaiter declares it, so a state machine + // awaiting one always calls UnsafeOnCompleted - which must not ask the user awaiter for an interface map it + // cannot supply. + [Fact] + public async Task EnumerateAsync_AwaiterImplementingOnlyINotifyCompletion() + { + var values = new List(); + await foreach (var value in DynamicAwaitHelper.EnumerateBenchmarkAsync(new PolitelyAwaitableSource(), Info())) + values.Add(value); + + Assert.Equal([1, 2], values); + } + + // The awaiter pattern binds on the awaiter's *declared* type, which may be an interface. Nothing can be asked + // for an interface map about one, so the map lookup must not be reached for it. + [Fact] + public async Task EnumerateAsync_AwaiterDeclaredAsAnInterface() + { + var values = new List(); + await foreach (var value in DynamicAwaitHelper.EnumerateBenchmarkAsync(new InterfaceAwaitedSource(), Info())) + values.Add(value); + + Assert.Equal([1, 2], values); + } + + private static AsyncEnumerableInfo Info() + { + Assert.True(typeof(T).IsAsyncEnumerable(out var info)); + return info!; + } + + private sealed class PolitelyAwaitableSource + { + public PoliteEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new(); + } + + private sealed class PoliteEnumerator + { + private int index; + public int Current => index; + public PoliteAwaitable MoveNextAsync() => new(++index <= 2); + } + + private readonly struct PoliteAwaitable(bool result) + { + public PoliteAwaiter GetAwaiter() => new(result); + } + + // Deliberately NOT ICriticalNotifyCompletion - the awaiter pattern does not require it. + private readonly struct PoliteAwaiter(bool result) : INotifyCompletion + { + public bool IsCompleted => false; + public bool GetResult() => result; + public void OnCompleted(Action continuation) => ThreadPool.QueueUserWorkItem(_ => continuation()); + } + + private sealed class InterfaceAwaitedSource + { + public InterfaceAwaitedEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new(); + } + + private sealed class InterfaceAwaitedEnumerator + { + private int index; + public int Current => index; + public InterfaceAwaitable MoveNextAsync() => new(++index <= 2); + } + + private readonly struct InterfaceAwaitable(bool result) + { + // Declared as the interface: legal in the awaiter pattern, and it makes AwaiterType an interface. + public IBoolAwaiter GetAwaiter() => new BoolAwaiter(result); + } + + private interface IBoolAwaiter : INotifyCompletion + { + bool IsCompleted { get; } + bool GetResult(); + } + + // Implements INotifyCompletion explicitly, so this also covers the reason the interface map was consulted: + // dispatching through the interface method has to reach an explicit implementation. + private sealed class BoolAwaiter(bool result) : IBoolAwaiter + { + public bool IsCompleted => false; + public bool GetResult() => result; + void INotifyCompletion.OnCompleted(Action continuation) => ThreadPool.QueueUserWorkItem(_ => continuation()); + } +} diff --git a/tests/BenchmarkDotNet.Tests/Order/DefaultOrdererTests.cs b/tests/BenchmarkDotNet.Tests/Order/DefaultOrdererTests.cs index 30147c136a..35c7379dae 100644 --- a/tests/BenchmarkDotNet.Tests/Order/DefaultOrdererTests.cs +++ b/tests/BenchmarkDotNet.Tests/Order/DefaultOrdererTests.cs @@ -21,7 +21,7 @@ public class DefaultOrdererTests new Job(), new ParameterInstances( [ - new ParameterInstance(new ParameterDefinition("P", false, [], false, parameterType: null!, 0), parameter, SummaryStyle.Default) + new ParameterInstance(new ParameterDefinition("P", isStatic: false, isArgument: false, parameterType: null!, 0), parameter, SummaryStyle.Default) ]), DefaultConfig.Instance.AddLogicalGroupRules(rules).CreateImmutableConfig() ); diff --git a/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs b/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs index f46e2387b6..a114578e61 100644 --- a/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs +++ b/tests/BenchmarkDotNet.Tests/ParameterComparerTests.cs @@ -4,7 +4,7 @@ namespace BenchmarkDotNet.Tests { public class ParameterComparerTests { - private static readonly ParameterDefinition sharedDefinition = new ParameterDefinition("Testing", isStatic: false, values: [], isArgument: false, parameterType: null!, 0); + private static readonly ParameterDefinition sharedDefinition = new ParameterDefinition("Testing", isStatic: false, isArgument: false, parameterType: null!, 0); [Fact] public void BasicComparisionTest() diff --git a/tests/BenchmarkDotNet.Tests/ParameterInstanceTests.cs b/tests/BenchmarkDotNet.Tests/ParameterInstanceTests.cs index 3169bb41c2..d0401641b1 100644 --- a/tests/BenchmarkDotNet.Tests/ParameterInstanceTests.cs +++ b/tests/BenchmarkDotNet.Tests/ParameterInstanceTests.cs @@ -5,7 +5,7 @@ namespace BenchmarkDotNet.Tests { public class ParameterInstanceTests { - private static readonly ParameterDefinition definition = new ParameterDefinition("Testing", isStatic: false, values: [], isArgument: false, parameterType: null!, 0); + private static readonly ParameterDefinition definition = new ParameterDefinition("Testing", isStatic: false, isArgument: false, parameterType: null!, 0); [Theory] [InlineData(5)] diff --git a/tests/BenchmarkDotNet.Tests/ParamsSourceTests.cs b/tests/BenchmarkDotNet.Tests/ParamsSourceTests.cs index aba37c1c19..83431d7627 100644 --- a/tests/BenchmarkDotNet.Tests/ParamsSourceTests.cs +++ b/tests/BenchmarkDotNet.Tests/ParamsSourceTests.cs @@ -1,4 +1,5 @@ using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Parameters; using BenchmarkDotNet.Running; namespace BenchmarkDotNet.Tests @@ -29,6 +30,60 @@ public class ParamsSourceWithNull public object? FooBar() => O; } + [Fact] + public void AsyncEnumerableNullParamsSourceIsResolvedAtDiscovery() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(AsyncEnumerableNullParams)).BenchmarksCases; + + var values = benchmarks + .Select(b => b.Parameters.Items.Single(p => p.Name == nameof(AsyncEnumerableNullParams.Value)).Value) + .ToArray(); + + Assert.Equal(new object?[] { null, "x" }, values); + } + + public class AsyncEnumerableNullParams + { + public static async IAsyncEnumerable Values() + { + await Task.Yield(); + yield return null; + yield return "x"; + } + + [ParamsSource(nameof(Values))] + public object? Value { get; set; } + + [Benchmark] + public object? Run() => Value; + } + + [Fact] + public void AsyncEnumerableNullArgumentsSourceIsResolvedAtDiscovery() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(AsyncEnumerableNullArguments)).BenchmarksCases; + + var values = benchmarks + .Select(b => b.Parameters.Items.Single().Value) + .ToArray(); + + Assert.Equal(new object?[] { null, "x" }, values); + } + + public class AsyncEnumerableNullArguments + { + public static async IAsyncEnumerable Arguments() + { + await Task.Yield(); + yield return null; + yield return "x"; + } + + [Benchmark] + [ArgumentsSource(nameof(Arguments))] + public object? Run(object? argument) => argument; + } + // #2980 [Fact] public void WriteOnlyPropertyDoesThrowNullReferenceException() @@ -57,5 +112,647 @@ public int WriteOnlyValues [Benchmark] public void Run() { } } + + [Fact] + public void AsyncEnumerableParamsSourceIsResolvedAtDiscovery() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(AsyncEnumerableParams)).BenchmarksCases; + + var values = benchmarks + .Select(b => b.Parameters.Items.Single(p => p.Name == nameof(AsyncEnumerableParams.Value)).Value) + .ToArray(); + + Assert.Equal(new object[] { 1, 2, 3 }, values); + } + + public class AsyncEnumerableParams + { + public static async IAsyncEnumerable Values() + { + await Task.Yield(); + yield return 1; + yield return 2; + yield return 3; + } + + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + [Benchmark] + public int Run() => Value; + } + + [Fact] + public void AsyncEnumerableValueTypeParamsSourceIsResolvedAtDiscovery() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(AsyncEnumerableValueTypeParams)).BenchmarksCases; + + var values = benchmarks + .Select(b => b.Parameters.Items.Single(p => p.Name == nameof(AsyncEnumerableValueTypeParams.Value)).Value) + .ToArray(); + + Assert.Equal(new object[] { 1, 2, 3 }, values); + } + + public class AsyncEnumerableValueTypeParams + { + public static async IAsyncEnumerable Values() + { + await Task.Yield(); + yield return 1; + yield return 2; + yield return 3; + } + + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + [Benchmark] + public int Run() => Value; + } + + [Fact] + public void AsyncEnumerableSourceWithOptionalParametersIsResolvedAtDiscovery() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(AsyncEnumerableOptionalParams)).BenchmarksCases; + + var values = benchmarks + .Select(b => b.Parameters.Items.Single(p => p.Name == nameof(AsyncEnumerableOptionalParams.Value)).Value) + .ToArray(); + + Assert.Equal(new object[] { 1, 2 }, values); + } + + public class AsyncEnumerableOptionalParams + { + public static async IAsyncEnumerable Values( + [System.Runtime.CompilerServices.EnumeratorCancellation] System.Threading.CancellationToken cancellationToken = default) + { + await Task.Yield(); + yield return 1; + yield return 2; + } + + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + [Benchmark] + public int Run() => Value; + } + + [Fact] + public void ParamsSourceWithOptionalParameterWithoutDefaultIsResolved() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(OptionalWithoutDefaultParams)).BenchmarksCases; + + var values = benchmarks + .Select(b => b.Parameters.Items.Single(p => p.Name == nameof(OptionalWithoutDefaultParams.Value)).Value) + .ToArray(); + + Assert.Equal(new object[] { 0, 1 }, values); + } + + public class OptionalWithoutDefaultParams + { + // Optional without a declared default: BDN must pass default(T), since Invoke does no optional binding. + public static IEnumerable Values([System.Runtime.InteropServices.Optional] int start) + { + yield return start; + yield return start + 1; + } + + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + [Benchmark] + public int Run() => Value; + } + + [Theory] + // A reference-type element is read through IAsyncEnumerable's covariance and a value-type one through + // reflection, which wraps whatever the source threw in a TargetInvocationException. What the user sees must + // not depend on that: both surface the exception as thrown. + [InlineData(typeof(ThrowingAsyncSource.OfReferenceType))] + [InlineData(typeof(ThrowingAsyncSource.OfValueType))] + public void AThrowingAsyncSourceSurfacesItsOwnException(Type benchmarkType) + { + var exception = Assert.Throws(() => BenchmarkConverter.TypeToBenchmarks(benchmarkType)); + + Assert.Equal("from the source", exception.Message); + } + + public static class ThrowingAsyncSource + { + public class OfReferenceType + { + public static async IAsyncEnumerable Values() + { + await Task.Yield(); + yield return default!; + + // Discovery reads the whole sequence, so the next move reaches this. + throw new InvalidTimeZoneException("from the source"); + } + + [Benchmark][ArgumentsSource(nameof(Values))] public object Run(object a) => a; + } + + public class OfValueType + { + public static async IAsyncEnumerable Values() + { + await Task.Yield(); + yield return default!; + + // Discovery reads the whole sequence, so the next move reaches this. + throw new InvalidTimeZoneException("from the source"); + } + + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(int a) => a; + } + } + + [Fact] + public void OneArgumentTakingTheTypeParameterItselfIsAccepted() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(MatchedGenericArgumentSource)).BenchmarksCases; + + var parameter = Assert.Single(Assert.Single(benchmarks).Parameters.Items); + Assert.Equal(7, parameter.Value); + } + + [Fact] + public void OneArgumentTakingTheTypeParameterByImplicitConversionIsAccepted() + { + // ReadOnlySpan never accepts byte[] by assignability, only through its implicit conversion - + // nothing is indexed, so the declaration holds whatever T is. + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(SpanFromGenericArgumentSource)).BenchmarksCases; + + Assert.Single(Assert.Single(benchmarks).Parameters.Items); + } + + [Theory] + [InlineData(typeof(WholeArrayByConversion.ToReadOnlySpan))] + [InlineData(typeof(WholeArrayByConversion.ToReadOnlyMemory))] + [InlineData(typeof(WholeArrayByConversion.ToMemory))] +#if !NETFRAMEWORK + [InlineData(typeof(WholeArrayByConversion.ToArraySegment))] +#endif + [InlineData(typeof(WholeArrayByConversion.ToRefArray))] + public void AnArrayAParameterIsDeclaredAsOrBuiltFromIsNotAnArgumentList(Type benchmarkType) + { + var parameter = Assert.Single(Assert.Single(BenchmarkConverter.TypeToBenchmarks(benchmarkType).BenchmarksCases).Parameters.Items); + + Assert.Equal(new byte[] { 1, 2, 3 }, Assert.IsType(parameter.Value)); + } + + // A typed array is not an object[], so none of these reach the branch that reads an element as an + // argument list, whatever conversion the parameter goes on to apply to the array it is handed. + public static class WholeArrayByConversion + { + public class ToReadOnlySpan + { + public static IEnumerable Values() { yield return [1, 2, 3]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(ReadOnlySpan a) => a.Length; + } + + public class ToReadOnlyMemory + { + public static IEnumerable Values() { yield return [1, 2, 3]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(ReadOnlyMemory a) => a.Length; + } + + public class ToMemory + { + public static IEnumerable Values() { yield return [1, 2, 3]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(Memory a) => a.Length; + } + +#if !NETFRAMEWORK + public class ToArraySegment + { + public static IEnumerable Values() { yield return [1, 2, 3]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(ArraySegment a) => a.Count; + } +#endif + + public class ToRefArray + { + public static IEnumerable Values() { yield return [1, 2, 3]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(ref byte[] a) => a.Length; + } + } + + // Every row is one cell of the matrix this rule was measured against, and the rows must keep disagreeing + // along both axes: which interface the source is *written* as, and which branch of CreateForArguments + // took the value. Two candidates were tried and rejected, and each survives a theory that varies only one + // axis - reading the declared element type instead flips the two List rows, and additionally + // requiring the value to have come out of the array flips the two written-as-the-interface rows that fall + // through. Keep at least one row on each side of both. + [Theory] + // Written as the interface, per-item branch: indexed, and every toolchain agrees. + [InlineData(typeof(ArgumentListSource.Declared), 0)] + // Written as the interface, unwrap branch - the element is the argument. + [InlineData(typeof(ArgumentListSource.DeclaredUnwrapped), 0)] + // Written as the interface but falling through, so the value is the whole array while the generated code + // indexes it. Master's defect, and the reason the index cannot be read from the branch alone. + [InlineData(typeof(ArgumentListSource.DeclaredUnrecognised), 0)] + [InlineData(typeof(ArgumentListSource.DeclaredTooManyForOne), 0)] + // Only implementing it, so it is not indexed - which costs the multi-argument case an index it needs. + // Master's defect, and the reason the index cannot be read from the element type alone. + [InlineData(typeof(ArgumentListSource.Implemented), null)] + // Only implementing it, and falling through: the whole array is the value and nothing indexes it. + [InlineData(typeof(ArgumentListSource.WholeArray), null)] + // The async half follows the same rule, and has to: rewriting a source from IEnumerable to + // IAsyncEnumerable may not change how its elements map onto arguments. There is no master + // behaviour to reproduce on this side, so these three rows are what holds the two halves together. + // Widening only the async side would not remove a defect, it would move one: AsyncImplemented would gain + // the index it wants, and AsyncWholeArray would lose the agreement it has - and of the two, the whole-array + // cell is the one that fails silently, with the generated code indexing what the in-process toolchains hand + // over whole. See the note on SmartParamBuilder.Indexes. + [InlineData(typeof(ArgumentListSource.AsyncDeclared), 0)] + [InlineData(typeof(ArgumentListSource.AsyncImplemented), null)] + [InlineData(typeof(ArgumentListSource.AsyncWholeArray), null)] + public void TheIndexFollowsTheInterfaceTheSourceIsWrittenAs(Type benchmarkType, int? expected) + { + var parameter = Assert.Single(BenchmarkConverter.TypeToBenchmarks(benchmarkType).BenchmarksCases).Parameters.Items.First(); + + Assert.Equal(expected, Assert.IsType(parameter.ParameterValue).ElementIndex); + } + + // The erased-enum display branch was reached only by attribute constants before this work, so the value + // was always the enum's underlying type. A source can yield anything, and Enum.ToObject throws on anything + // else - out of ToDisplayText, which runs while logging rather than while validating. + // The arguments of one row come out of a single read, which is what lets a toolchain emit the read once + // and index into it. Two reads that merely compare equal would not do: the renderer groups by identity. + [Fact] + public void ArgumentsOfOneRowShareOneRead() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(TwoArgumentsFromOneSource)).BenchmarksCases; + + foreach (var benchmark in benchmarks) + { + var reads = benchmark.Parameters.Items + .Select(parameter => Assert.IsType(parameter.ParameterValue).Read) + .ToArray(); + + Assert.Equal(2, reads.Length); + Assert.Same(reads[0], reads[1]); + } + + // ... and a different row is a different read. + Assert.NotSame( + Assert.IsType(benchmarks.First().Parameters.Items.First().ParameterValue).Read, + Assert.IsType(benchmarks.Last().Parameters.Items.First().ParameterValue).Read); + } + + public class TwoArgumentsFromOneSource + { + public class Box { public int Value { get; set; } } + + public static IEnumerable Rows() + { + yield return [new Box { Value = 1 }, new Box { Value = 10 }]; + yield return [new Box { Value = 2 }, new Box { Value = 20 }]; + } + + [Benchmark][ArgumentsSource(nameof(Rows))] public int Run(Box a, Box b) => a.Value + b.Value; + } + + [Fact] + public void AnEnumParameterFedAValueThatIsNotItsUnderlyingTypeStillRenders() + { + var benchmark = Assert.Single(BenchmarkConverter.TypeToBenchmarks(typeof(MismatchedEnumSource)).BenchmarksCases); + + Assert.Contains("not an enum", benchmark.DisplayInfo); + } + + // The reason the branch exists: F# erases an enum to its underlying type, so the declared type names it. + [Fact] + public void AnEnumParameterFedItsUnderlyingTypeRendersTheEnumName() + { + var benchmark = Assert.Single(BenchmarkConverter.TypeToBenchmarks(typeof(ErasedEnumSource)).BenchmarksCases); + + Assert.Contains("Green", benchmark.DisplayInfo); + } + + public enum Colour { Red = 1, Green = 2 } + + public class MismatchedEnumSource + { + [ParamsSource(nameof(Values))] + public Colour Value { get; set; } + + public static IEnumerable Values() { yield return "not an enum"; } + + [Benchmark] public int Run() => (int) Value; + } + + public class ErasedEnumSource + { + [ParamsSource(nameof(Values))] + public Colour Value { get; set; } + + public static IEnumerable Values() { yield return 2; } + + [Benchmark] public int Run() => (int) Value; + } + + public static class ArgumentListSource + { + public class Declared + { + public static IEnumerable Values() { yield return [new Box(), new Box()]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(Box a, Box b) => 0; + } + + public class DeclaredUnwrapped + { + public static IEnumerable Values() { yield return [new object[] { 1, 2 }]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(object[] a) => a.Length; + } + + // One element for one parameter, but its runtime type is not the parameter's, so the single-argument + // branch declines it and the whole array is handed over. + public class DeclaredUnrecognised + { + public static IEnumerable Values() { yield return [new BoxImpl()]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(IBox a) => 0; + } + + public class DeclaredTooManyForOne + { + public static IEnumerable Values() { yield return [1, 2, 3]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(object[] a) => a.Length; + } + + public class Implemented + { + public static List Values() => [[new Box(), new Box()]]; + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(Box a, Box b) => 0; + } + + public class WholeArray + { + public static List Values() => [[1, 2, 3]]; + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(object[] a) => a.Length; + } + + public class AsyncDeclared + { + public static async IAsyncEnumerable Values() { await Task.Yield(); yield return [new Box(), new Box()]; } + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(Box a, Box b) => 0; + } + + public class AsyncImplemented + { + public static ArrayRows Values() => new([new Box(), new Box()]); + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(Box a, Box b) => 0; + } + + public class AsyncWholeArray + { + public static ArrayRows Values() => new([1, 2, 3]); + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(object[] a) => a.Length; + } + + // Implements the interface without being written as it - the async counterpart of List. + public sealed class ArrayRows(object[] row) : IAsyncEnumerable + { + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + await Task.Yield(); + yield return row; + } + } + + public class Box { } + + public interface IBox { } + + public class BoxImpl : IBox { } + } + + [GenericTypeArguments(typeof(int))] + public class MatchedGenericArgumentSource + { + public static IEnumerable Values() { yield return (T)(object)7; } + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public T Run(T a) => a; + } + + [GenericTypeArguments(typeof(byte[]))] + public class SpanFromGenericArgumentSource + { + public static IEnumerable Values() { yield return (T)(object)new byte[] { 1, 2, 3 }; } + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public int Run(ReadOnlySpan bytes) => bytes.Length; + } + + [Fact] + public void AnInheritedGenericSourceFeedingItsOwnTypeParameterIsAccepted() + { + // The same reading has to keep this one working: the base's T and the benchmark's T are the same + // parameter once the base is written as the derived type names it. + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(InheritedMatchedSource)).BenchmarksCases; + + Assert.Single(Assert.Single(benchmarks).Parameters.Items); + } + + public class GenericSourceBase + { + public static IEnumerable Values() { yield return default!; } + } + + public class InheritedMatchedSource : GenericSourceBase + { + [Benchmark][ArgumentsSource(nameof(Values))] public T Run(T a) => a; + } + + [Fact] + public void AGenericSourceMethodIsRejectedWithItsOwnMessage() + { + // Reflection would otherwise fail with "Late bound operations cannot be performed on types or methods + // for which ContainsGenericParameters is true", which names nothing the user wrote. + var exception = Assert.Throws( + () => BenchmarkConverter.TypeToBenchmarks(typeof(GenericSourceMethod))); + + Assert.Contains("is generic", exception.Message); + Assert.Contains(nameof(GenericSourceMethod.Values), exception.Message); + } + +#pragma warning disable BDN1310 + public class GenericSourceMethod + { + public static IEnumerable Values() { yield return default!; } + + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(int a) => a; + } +#pragma warning restore BDN1310 + + [Fact] + public void APropertyIsPreferredOverAGenericMethodOfTheSameName() + { + // The generic method cannot be invoked, but it does not speak for the name: the property serves it. + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(GenericMethodHidingAProperty)).BenchmarksCases; + + Assert.Equal([7], benchmarks.Select(benchmark => benchmark.Parameters.Items.Single().Value)); + } + + public class PropertySourceBase + { + public static IEnumerable Values => [7]; + } + + public class GenericMethodHidingAProperty : PropertySourceBase + { + public static new IEnumerable Values() { yield return default!; } + + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(int a) => a; + } + + [Fact] + public void ASourceNamedThroughItsOwnBaseTypeIsReadAsWritten() + { + // typeof(Base) fixes the source's type arguments, so it is not in the benchmark's generic context + // even though the benchmark derives from that same base. Judging it there would compare int against U. + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(NamedThroughOwnBase)).BenchmarksCases; + + Assert.Single(Assert.Single(benchmarks).Parameters.Items); + } + + public class NamedThroughOwnBase : GenericSourceBase + { + [Benchmark] + [ArgumentsSource(typeof(GenericSourceBase), nameof(GenericSourceBase.Values))] + public U Run(U a) => a; + } + + [Fact] + public void ANonGenericOverloadIsPreferredOverAGenericOne() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(GenericAndNonGenericSourceOverloads)).BenchmarksCases; + + Assert.Equal([7], benchmarks.Select(benchmark => benchmark.Parameters.Items.Single().Value)); + } + + public class GenericAndNonGenericSourceOverloads + { + public static IEnumerable Values() { yield return default!; } + + public static IEnumerable Values() { yield return 7; } + + [Benchmark][ArgumentsSource(nameof(Values))] public int Run(int a) => a; + } + + [Theory] + // A ref/in/out parameter reaches reflection as a byref type - `ref T` is `T&` - which nothing is castable to. + // The modifier says how the argument travels, not what it has to be, so the comparison looks past it. + [InlineData(typeof(RefParameterFromT))] + [InlineData(typeof(InParameterFromT))] + public void ARefModifierDoesNotChangeWhatTheParameterTakes(Type benchmarkType) + { + var benchmark = Assert.Single(BenchmarkConverter.TypeToBenchmarks(benchmarkType).BenchmarksCases); + + // The source yields default(T); the point is that the by-ref parameter produced a parameter at all. + Assert.Equal(0, Assert.Single(benchmark.Parameters.Items).Value); + } + + public class RefParameterFromT + { + public static IEnumerable Values() { yield return default!; } + [Benchmark][ArgumentsSource(nameof(Values))] public T Run(ref T a) => a; + } + + public class InParameterFromT + { + public static IEnumerable Values() { yield return default!; } + [Benchmark][ArgumentsSource(nameof(Values))] public T Run(in T a) => a; + } + + [Fact] + public void AsyncDeclaredSourceIsReadAsynchronouslyEvenWhenTheValueIsAlsoEnumerable() + { + // Discovery must bind what the generated code binds - the declared IAsyncEnumerable - rather than the + // non-generic IEnumerable the returned object happens to also implement. + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(DualShapedSourceParams)).BenchmarksCases; + + var values = benchmarks + .Select(b => b.Parameters.Items.Single(p => p.Name == nameof(DualShapedSourceParams.Value)).Value) + .ToArray(); + + Assert.Equal(new object[] { "async" }, values); + } + + public class DualShapedSourceParams + { + public static IAsyncEnumerable Values() => new DualShaped(); + + [ParamsSource(nameof(Values))] + public string Value { get; set; } = null!; + + [Benchmark] + public string Run() => Value; + + // Async-first collection that also exposes a synchronous view, as a hand-written one often does. + private sealed class DualShaped : IAsyncEnumerable, System.Collections.IEnumerable + { + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + await Task.Yield(); + yield return "async"; + } + + public System.Collections.IEnumerator GetEnumerator() + { + yield return "sync"; + } + } + } + + [Fact] + public void AsyncEnumerablePatternParamsSourceIsRejected() + { + var exception = Assert.Throws( + () => BenchmarkConverter.TypeToBenchmarks(typeof(AsyncEnumerablePatternParams))); + + Assert.Contains(nameof(AsyncEnumerablePatternParams.Values), exception.Message); + Assert.Contains("does not implement IEnumerable or IAsyncEnumerable", exception.Message); + } + +#pragma warning disable BDN1306 + public class AsyncEnumerablePatternParams + { + // A custom await-foreach shape that does NOT implement IAsyncEnumerable. + public sealed class PatternEnumerable + { + public PatternEnumerator GetAsyncEnumerator(System.Threading.CancellationToken token = default) => new(); + } + + public sealed class PatternEnumerator + { + private int index = -1; + private readonly int[] items = [10, 20]; + public int Current => items[index]; + public async ValueTask MoveNextAsync() + { + await Task.Yield(); + return ++index < items.Length; + } + } + + public static PatternEnumerable Values() => new(); + + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + [Benchmark] + public int Run() => Value; + } +#pragma warning restore BDN1306 } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/RefStructSourceTests.cs b/tests/BenchmarkDotNet.Tests/RefStructSourceTests.cs new file mode 100644 index 0000000000..c475a9d39a --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/RefStructSourceTests.cs @@ -0,0 +1,124 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using System.Collections; + +// The framework's IEnumerable only took an allows-ref-struct type parameter in .NET 10, and the constraint +// itself needs RuntimeFeature.ByRefLikeGenerics, which .NET Framework does not have. Every declaration in this +// file is one of those shapes; the rules they cover are not framework-specific. +#if NET10_0_OR_GREATER +namespace BenchmarkDotNet.Tests; + +// Since .NET 10 the framework's IEnumerable allows a ref struct type argument, so a source can be declared to +// yield one. Reading the values puts each into an object[], which a ref struct cannot enter, so the enumeration +// fails inside reflection saying nothing about the benchmark. Discovery names it, and BDN1311 reports it at build time. +public class RefStructSourceTests +{ + [Fact] + public void DiscoveryReportsARefStructElement() + { + var exception = Assert.Throws( + () => BenchmarkConverter.TypeToBenchmarks(typeof(YieldsRefStruct))); + + Assert.Contains("which is a ref struct", exception.Message); + Assert.Contains("Span", exception.Message); + } + + public class YieldsRefStruct + { + public IEnumerable> Data() => new SpanSequence(); + + [Benchmark] +#pragma warning disable BDN1311 + [ArgumentsSource(nameof(Data))] +#pragma warning restore BDN1311 + public int Run(Span argument) => argument.Length; + } + + // An async source reads its values into the same object[], so the shape is asked of both and ahead of either. + [Fact] + public void DiscoveryReportsARefStructElementFromAnAsyncSource() + { + var exception = Assert.Throws( + () => BenchmarkConverter.TypeToBenchmarks(typeof(YieldsRefStructAsynchronously))); + + Assert.Contains("which is a ref struct", exception.Message); + Assert.Contains("Span", exception.Message); + } + + public class YieldsRefStructAsynchronously + { + public IAsyncEnumerable> Data() => new AsyncSpanSequence(); + + [Benchmark] +#pragma warning disable BDN1311 + [ArgumentsSource(nameof(Data))] +#pragma warning restore BDN1311 + public int Run(Span argument) => argument.Length; + } + + // The substitution, not the declaration, decides this: a source declared to yield a type parameter that merely + // admits a ref struct is read like any other value type when closed to one that is not, and named here when it + // is. SourceReturnTypeValidator therefore leaves the shape alone - judging the open declaration would report + // only the substitutions that work. + [Fact] + public void DiscoveryReportsARefStructSubstitution() + { + var exception = Assert.Throws( + () => BenchmarkConverter.TypeToBenchmarks(typeof(AdmitsARefStruct>))); + + Assert.Contains("which is a ref struct", exception.Message); + Assert.Contains("Span", exception.Message); + } + + [Fact] + public void ANonRefStructSubstitutionIsAccepted() + => Assert.NotEmpty(BenchmarkConverter.TypeToBenchmarks(typeof(AdmitsARefStruct)).BenchmarksCases); + + public class AdmitsARefStruct where T : allows ref struct + { + public static IEnumerable Values() => new OneValue(); + + [Benchmark] +#pragma warning disable BDN1312 + [ArgumentsSource(nameof(Values))] +#pragma warning restore BDN1312 + public void Run(T value) { } + } + + private sealed class OneValue : IEnumerable, IEnumerator where T : allows ref struct + { + private int index = -1; + + public T Current => default!; + object IEnumerator.Current => null!; + + public IEnumerator GetEnumerator() => new OneValue(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool MoveNext() => ++index == 0; + public void Reset() => index = -1; + public void Dispose() { } + } + + private sealed class AsyncSpanSequence : IAsyncEnumerable>, IAsyncEnumerator> + { + private int index; + public Span Current => new int[] { 1, 2, 3 }; + public IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default) => this; + public ValueTask MoveNextAsync() => new(index++ < 1); + public ValueTask DisposeAsync() => default; + } + + private sealed class SpanSequence : IEnumerable>, IEnumerator> + { + private int index; + public Span Current => new int[] { 1, 2, 3 }; + object IEnumerator.Current => throw new NotSupportedException(); + public IEnumerator> GetEnumerator() => this; + IEnumerator IEnumerable.GetEnumerator() => this; + public bool MoveNext() => index++ < 1; + public void Reset() => index = 0; + public void Dispose() { } + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/ReflectionTests.cs b/tests/BenchmarkDotNet.Tests/ReflectionTests.cs index e39c71af82..904b6e2a07 100644 --- a/tests/BenchmarkDotNet.Tests/ReflectionTests.cs +++ b/tests/BenchmarkDotNet.Tests/ReflectionTests.cs @@ -2,6 +2,7 @@ using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Tests.XUnit; using JetBrains.Annotations; +using System.Reflection; namespace BenchmarkDotNet.Tests { @@ -202,6 +203,55 @@ public class WithImplicitCastToStackOnlyStruct public static implicit operator StackOnlyStruct(WithImplicitCastToStackOnlyStruct instance) => new StackOnlyStruct { Span = instance.Array }; } + // The declared-type and cross-kind assertions below all fail without their part of the lookup. The + // same-kind most-derived ones cannot: reflection is documented as returning members in no particular + // order, but every runtime tested yields the derived member first, so a first match passes here too. + // Those are asserted to pin the intent, not because this test can catch their absence. + [Fact] + public void GetParameterMemberPrefersTheMostDerivedOfAHiddenPair() + { + const BindingFlags Instance = BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy; + const BindingFlags Static = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy; + + Assert.Equal(typeof(HidingMembers), typeof(HidingMembers).GetParameterMember("Field", typeof(string), Instance)?.DeclaringType); + Assert.Equal(typeof(HidingMembers), typeof(HidingMembers).GetParameterMember("StaticField", typeof(string), Static)?.DeclaringType); + + // A field hiding a property and a property hiding a field: looking either kind up first finds the base + // member of that kind, which is not the parameter. + Assert.IsAssignableFrom(typeof(HidingMembers).GetParameterMember("PropertyHiddenByField", typeof(string), Instance)); + Assert.IsAssignableFrom(typeof(HidingMembers).GetParameterMember("StaticPropertyHiddenByField", typeof(string), Static)); + Assert.IsAssignableFrom(typeof(HidingMembers).GetParameterMember("FieldHiddenByProperty", typeof(string), Instance)); + + // The declared type still selects: only the base declares an int of that name. + Assert.Equal(typeof(HiddenMembers), typeof(HidingMembers).GetParameterMember("Typed", typeof(int), Instance)?.DeclaringType); + Assert.Equal(typeof(HidingMembers), typeof(HidingMembers).GetParameterMember("Typed", typeof(string), Instance)?.DeclaringType); + + // An indexer takes arguments and is never a parameter member. + Assert.Null(typeof(HidingMembers).GetParameterMember("Item", typeof(string), Instance)); + Assert.Null(typeof(HidingMembers).GetParameterMember("Field", typeof(int), Instance)); + } + + public class HiddenMembers + { + public string Field = ""; + public static string StaticField = ""; + public int Typed; + public string PropertyHiddenByField { get; set; } = ""; + public static string StaticPropertyHiddenByField { get; set; } = ""; + public string FieldHiddenByProperty = ""; + } + + public class HidingMembers : HiddenMembers + { + public new string Field = ""; + public static new string StaticField = ""; + public new string Typed = ""; + public new string PropertyHiddenByField = ""; + public static new string StaticPropertyHiddenByField = ""; + public new string FieldHiddenByProperty { get; set; } = ""; + + public string this[int index] => ""; + } } } diff --git a/tests/BenchmarkDotNet.Tests/Shared/Polyfills/AsyncEnumerableExtensions.cs b/tests/BenchmarkDotNet.Tests/Shared/Polyfills/AsyncEnumerableExtensions.cs new file mode 100644 index 0000000000..ab6bd0281a --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Shared/Polyfills/AsyncEnumerableExtensions.cs @@ -0,0 +1,38 @@ +#if !NET10_0_OR_GREATER +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Linq; + +// System.Linq.AsyncEnumerable ships in the shared framework from .NET 10, and BenchmarkDotNet polyfills the two +// members it needs rather than taking the package - see Extensions/Polyfills/AsyncEnumerable.cs for why. The tests +// see those two through InternalsVisibleTo; ToArrayAsync is theirs alone, so it lives here. A separate class, not +// more members on AsyncEnumerable: that name is already taken by the one they inherit. +internal static class AsyncEnumerableExtensions +{ + internal static async ValueTask ToArrayAsync( + this IAsyncEnumerable source, + CancellationToken cancellationToken = default) + { + List items = []; + + await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + items.Add(item); + } + + return items.ToArray(); + } + + internal static async IAsyncEnumerable Select( + this IAsyncEnumerable source, + Func selector) + { + await foreach (var item in source.ConfigureAwait(false)) + { + yield return selector(item); + } + } +} +#endif diff --git a/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs index 2339492b64..17917547a3 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/ExecutionValidatorTests.cs @@ -617,6 +617,86 @@ public async Task IteratorBodyExceptionsInAsyncEnumerableBenchmarksAreDiscovered Assert.Contains(validationErrors, error => error.Message.Contains("This iterator throws")); } + [Fact] + public async Task ARefStructCurrentIsNotValidatedAndNotRefused() + { + var validationErrors = await ExecutionValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(RefStructCurrentBenchmark))) + .ToArrayAsync(); + + // Says why it was skipped, and does not refuse it: IsCritical is what decides that, and FailOnError + // would otherwise turn the explanation into a refusal. + var skipped = Assert.Single(validationErrors); + Assert.Contains("yields by-ref-like elements", skipped.Message); + Assert.False(skipped.IsCritical); + } + + [Fact] + public async Task ARefStructParameterIsNotValidatedAndNotRefused() + { + var validationErrors = await ExecutionValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(RefStructParameterBenchmark))) + .ToArrayAsync(); + + var skipped = Assert.Single(validationErrors); + Assert.Contains("by-ref-like parameter", skipped.Message); + Assert.False(skipped.IsCritical); + } + + // The generated code passes a by-ref-like argument natively and runs; reflection cannot box one into the + // args array, so the validator can only skip - it must not refuse. + public class RefStructParameterBenchmark + { + public static IEnumerable Values() { yield return new byte[] { 1, 2, 3 }; } + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public int Run(ReadOnlySpan bytes) => bytes.Length; + } + + [Fact] + public async Task ARefStructReturnIsNotValidatedAndNotRefused() + { + var validationErrors = await ExecutionValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(RefStructReturnBenchmark))) + .ToArrayAsync(); + + var skipped = Assert.Single(validationErrors); + Assert.Contains("returns by-ref-like value", skipped.Message); + Assert.False(skipped.IsCritical); + } + + // Reflection cannot box a ref struct to hand it back, so the method cannot even be invoked - the guard has + // to come before the call, not around the result. + public class RefStructReturnBenchmark + { + [Benchmark] + public ReadOnlySpan Run() => default; + } + + // Both validators read Current through reflection, which cannot hand back a ref struct - so neither can + // validate this benchmark, and neither may refuse to run it. Its generated code reads Current strongly + // typed and works. + public class RefStructCurrentBenchmark + { + [Benchmark] + public SpanEnumerable Enumerating() => default; + + public readonly struct SpanEnumerable + { + public SpanEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new(); + } + + public struct SpanEnumerator + { + private int index; + + public ReadOnlySpan Current => default; + + public ValueTask MoveNextAsync() => new(index++ < 2); + } + } + public class ThrowingAsyncEnumerableBenchmark { [Benchmark] @@ -627,5 +707,25 @@ public async IAsyncEnumerable Throwing() throw new Exception("This iterator throws"); } } - } + + // An argument is passed to the benchmark method, so there is no member of that name to assign it to. + // Reporting one is a validation error against a benchmark that runs perfectly well. + [Fact] + public async Task ArgumentsAreNotMistakenForMembersToAssign() + { + var validationErrors = await ExecutionValidator.FailOnError.ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(WithArguments))).ToArrayAsync(); + + Assert.Empty(validationErrors); + } + + public class WithArguments + { + [Params(2)] + public int Parameter { get; set; } + + [Benchmark] + [Arguments(1)] + public int Bench(int value) => value + Parameter; + } +} } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Validators/ParamsValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/ParamsValidatorTests.cs index 4246586f8c..d22652d433 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/ParamsValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/ParamsValidatorTests.cs @@ -33,6 +33,18 @@ private async ValueTask Check(params string[] messageParts) Assert.Contains(messagePart, validationErrors.Single().Message); } + private async ValueTask CheckNoErrors() + { + var typeToBenchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(T)); + Assert.NotEmpty(typeToBenchmarks.BenchmarksCases); + + var validationErrors = await ParamsValidator.FailOnError.ValidateAsync(typeToBenchmarks).ToArrayAsync(); + foreach (var error in validationErrors) + output.WriteLine("* " + error.Message); + + Assert.Empty(validationErrors); + } + private const string P = "[Params]"; private const string Pa = "[ParamsAllValues]"; private const string Ps = "[ParamsSource]"; @@ -66,6 +78,8 @@ private async ValueTask Check(params string[] messageParts) [Fact] public async Task InternalProp1Test() => await Check(nameof(InternalProp1.Input), "setter is not public", P); [Fact] public async Task InternalProp2Test() => await Check(nameof(InternalProp2.Input), "setter is not public", Pa); [Fact] public async Task InternalProp3Test() => await Check(nameof(InternalProp3.Input), "setter is not public", Ps); + [Fact] public async Task ReservedName1Test() => await Check("__GlobalSetup", "reserved", P); + [Fact] public async Task ReservedName2Test() => await Check("__WorkloadActionUnroll", "reserved", Ps); public class Base { @@ -75,6 +89,22 @@ public void Foo() { } public static IEnumerable Source() => [false, true]; } + // A parameter member named like a generated member is rejected (the runnable's object initializer would bind to + // the generated member). Only [Params*] members collide - sources/arguments/non-parameter members don't. +#pragma warning disable BDN1208 + public class ReservedName1 : Base + { + [Params(1)] + public int __GlobalSetup { get; set; } + } + + public class ReservedName2 : Base + { + [ParamsSource(nameof(Base.Source))] + public bool __WorkloadActionUnroll { get; set; } + } +#pragma warning restore BDN1208 + #pragma warning disable BDN1205 public class Const1 : Base { @@ -319,33 +349,28 @@ public class PropMultiple4 : Base #if NET5_0_OR_GREATER - [Fact] public async Task InitOnly1Test() => await Check(nameof(InitOnly1.Input), "init-only", P); - [Fact] public async Task InitOnly2Test() => await Check(nameof(InitOnly2.Input), "init-only", Pa); - [Fact] public async Task InitOnly3Test() => await Check(nameof(InitOnly3.Input), "init-only", Ps); + // An init-only setter is assignable from the runnable's object initializer, so it is supported. + [Fact] public async Task InitOnly1Test() => await CheckNoErrors(); + [Fact] public async Task InitOnly2Test() => await CheckNoErrors(); + [Fact] public async Task InitOnly3Test() => await CheckNoErrors(); -#pragma warning disable BDN1206 public class InitOnly1 : Base { [Params(false, true)] public bool Input { get; init; } } -#pragma warning restore BDN1206 -#pragma warning disable BDN1206 public class InitOnly2 : Base { [ParamsAllValues] public bool Input { get; init; } } -#pragma warning restore BDN1206 -#pragma warning disable BDN1206 public class InitOnly3 : Base { [ParamsSource(nameof(Source))] public bool Input { get; init; } } -#pragma warning restore BDN1206 #endif } diff --git a/tests/BenchmarkDotNet.Tests/Validators/RequiredMemberValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/RequiredMemberValidatorTests.cs new file mode 100644 index 0000000000..c0fa1c449e --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Validators/RequiredMemberValidatorTests.cs @@ -0,0 +1,105 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Validators; +using System.Diagnostics.CodeAnalysis; + +namespace BenchmarkDotNet.Tests.Validators; + +public class RequiredMemberValidatorTests +{ + private static async ValueTask Validate() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(T)); + // Without a case to validate, every "is not reported" assertion below would hold for the wrong reason. + Assert.NotEmpty(benchmarks.BenchmarksCases); + + var errors = await RequiredMemberValidator.FailOnError.ValidateAsync(benchmarks).ToArrayAsync(); + return errors.Select(error => error.Message).ToArray(); + } + + [Fact] + public async Task RequiredMemberBenchmarkDotNetCannotSetIsReported() + { + var messages = await Validate(); + + Assert.Contains(messages, message => message.Contains(nameof(RequiredMemberWithoutAttribute.Text)) && message.Contains("required member")); + } + + [Fact] + public async Task RequiredParamsMemberIsNotReported() + { + var messages = await Validate(); + + Assert.DoesNotContain(messages, message => message.Contains("required member")); + } + + [Fact] + public async Task SetsRequiredMembersConstructorIsReported() + { + var messages = await Validate(); + + Assert.Contains(messages, message => message.Contains("[SetsRequiredMembers]")); + } + + [Fact] + public async Task PlainBenchmarkIsNotReported() + { + var messages = await Validate(); + + Assert.DoesNotContain(messages, message => message.Contains("[SetsRequiredMembers]")); + } + + [Fact] + public async Task NestedTypeDeclaringRequiredMembersIsNotReported() + { + // The compiler stamps [RequiredMember] on any type declaring required members, so a nested type must + // not be mistaken for a required member of the benchmark type. + var messages = await Validate(); + + Assert.Empty(messages); + } + + public class WithNestedTypeWithRequiredMember + { + public class Nested + { + public required int Value { get; set; } + } + + [Benchmark] + public void Foo() { } + } + +#pragma warning disable BDN1109 + public class RequiredMemberWithoutAttribute + { + public required string Text { get; set; } + + [Benchmark] + public void Foo() { } + } +#pragma warning restore BDN1109 + + public class RequiredParamsMember + { + [Params(1)] + public required int Value { get; set; } + + [Benchmark] + public void Foo() { } + } + +#pragma warning disable BDN1110 + public class WithSetsRequiredMembersCtor + { + [Params(1)] + public required int Value { get; set; } + + [SetsRequiredMembers] + public WithSetsRequiredMembersCtor() => Value = 1; + + [Benchmark] + public void Foo() { } + } +#pragma warning restore BDN1110 +} diff --git a/tests/BenchmarkDotNet.Tests/Validators/ReturnValueValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/ReturnValueValidatorTests.cs index 9c7a1d3718..8362fcf51e 100644 --- a/tests/BenchmarkDotNet.Tests/Validators/ReturnValueValidatorTests.cs +++ b/tests/BenchmarkDotNet.Tests/Validators/ReturnValueValidatorTests.cs @@ -337,5 +337,85 @@ private static async Task AssertInconsistent() return validationErrors; } - } + + [Fact] + public async Task ARefStructCurrentIsLeftOutOfTheComparisonRatherThanRefused() + { + // Comparing return values means holding every element, which a ref struct cannot be - so this benchmark + // has no comparable value. That leaves it out of the comparison; it must not fail it. + var validationErrors = await ReturnValueValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(RefStructCurrentBenchmark))) + .ToArrayAsync(); + + var skipped = Assert.Single(validationErrors); + Assert.Contains("yields by-ref-like elements", skipped.Message); + Assert.False(skipped.IsCritical); + } + + [Fact] + public async Task ARefStructParameterIsLeftOutOfTheComparisonRatherThanRefused() + { + var validationErrors = await ReturnValueValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(RefStructParameterBenchmark))) + .ToArrayAsync(); + + var skipped = Assert.Single(validationErrors); + Assert.Contains("by-ref-like parameter", skipped.Message); + Assert.False(skipped.IsCritical); + } + + // The generated code passes a by-ref-like argument natively and runs; reflection cannot box one into the + // args array, so the validator can only skip - it must not refuse. + public class RefStructParameterBenchmark + { + public static IEnumerable Values() { yield return new byte[] { 1, 2, 3 }; } + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public int Run(ReadOnlySpan bytes) => bytes.Length; + } + + [Fact] + public async Task ARefStructReturnIsLeftOutOfTheComparisonRatherThanRefused() + { + var validationErrors = await ReturnValueValidator.FailOnError + .ValidateAsync(BenchmarkConverter.TypeToBenchmarks(typeof(RefStructReturnBenchmark))) + .ToArrayAsync(); + + var skipped = Assert.Single(validationErrors); + Assert.Contains("returns by-ref-like value", skipped.Message); + Assert.False(skipped.IsCritical); + } + + // Reflection cannot box a ref struct to hand it back, so the method cannot even be invoked - the guard has + // to come before the call, not around the result. + public class RefStructReturnBenchmark + { + [Benchmark] + public ReadOnlySpan Run() => default; + } + + // Both validators read Current through reflection, which cannot hand back a ref struct - so neither can + // validate this benchmark, and neither may refuse to run it. Its generated code reads Current strongly + // typed and works. + public class RefStructCurrentBenchmark + { + [Benchmark] + public SpanEnumerable Enumerating() => default; + + public readonly struct SpanEnumerable + { + public SpanEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) => new(); + } + + public struct SpanEnumerator + { + private int index; + + public ReadOnlySpan Current => default; + + public ValueTask MoveNextAsync() => new(index++ < 2); + } + } +} } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.Tests/Validators/SourceReturnTypeValidatorTests.cs b/tests/BenchmarkDotNet.Tests/Validators/SourceReturnTypeValidatorTests.cs new file mode 100644 index 0000000000..8760ea0e4b --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Validators/SourceReturnTypeValidatorTests.cs @@ -0,0 +1,309 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Validators; + +#pragma warning disable BDN1306 +#pragma warning disable BDN1308 +#pragma warning disable BDN1311 +#pragma warning disable BDN1312 +#pragma warning disable BDN1504 + +namespace BenchmarkDotNet.Tests.Validators; + +public class SourceReturnTypeValidatorTests +{ + private static async ValueTask Validate() + { + var benchmarks = BenchmarkConverter.TypeToBenchmarks(typeof(T)); + // Without a case to validate, every "is not reported" assertion below would hold for the wrong reason. + Assert.NotEmpty(benchmarks.BenchmarksCases); + + var errors = await SourceReturnTypeValidator.FailOnError.ValidateAsync(benchmarks).ToArrayAsync(); + return errors.Select(error => error.Message).ToArray(); + } + + [Fact] + public async Task GenericEnumerableSourceIsNotReported() + { + Assert.Empty(await Validate()); + } + + [Fact] + public async Task NonGenericEnumerableSourceIsReported() + { + var messages = await Validate(); + + Assert.Contains(messages, message => message.Contains("neither IEnumerable nor IAsyncEnumerable")); + } + + [Fact] + public async Task SeveralEnumerableInstantiationsAreReported() + { + var messages = await Validate(); + + Assert.Contains(messages, message => message.Contains("more than one enumerable shape")); + } + + [Fact] + public async Task SeveralAsyncEnumerableInstantiationsAreReported() + { + var messages = await Validate(); + + Assert.Contains(messages, message => message.Contains("more than one enumerable shape")); + } + + // An instance source on an unrelated type is left alone, as master leaves it. The generated runnable reaches + // an instance source through `base`, which resolves nothing there, so a non-constant value does not compile + // out-of-process - but a source whose values are all compile-time constants is rendered inline and runs, on + // master and here. Reporting it would reject a shape master accepts, and that break is not this PR's to make. + [Fact] + public async Task InstanceSourceInAnotherTypeIsNotReported() + { + Assert.Empty(await Validate()); + } + + [Fact] + public async Task StaticSourceInAnotherTypeIsNotReported() + { + Assert.Empty(await Validate()); + } + + // Discovery keeps only the most derived declaration, so the hidden one's source is never read. Scanning + // members separately reported it anyway - and this validator is mandatory, so the benchmark could not run. + // A field, because reflection collapses a hidden property and hands back both declarations of a field. + [Fact] + public async Task ASourceOnAHiddenMemberIsNotReported() + { + Assert.Empty(await Validate()); + } + + public class BadParamsSourceOnABase + { + [ParamsSource(nameof(NotEnumerable))] public int Value; + + public static int NotEnumerable() => 0; + } + + public class HidesABadParamsSource : BadParamsSourceOnABase + { + [ParamsSource(nameof(Enumerable))] public new int Value; + + public static IEnumerable Enumerable() => [1]; + + [Benchmark] public int Run() => Value; + } + + public class SeparateSource + { + public IEnumerable Instance() => [1, 2]; + + public static IEnumerable Static() => [1, 2]; + } + + public class WithInstanceSourceInAnotherType + { + [Benchmark] + [ArgumentsSource(typeof(SeparateSource), nameof(SeparateSource.Instance))] + public void Run(int value) { } + } + + public class WithStaticSourceInAnotherType + { + [Benchmark] + [ArgumentsSource(typeof(SeparateSource), nameof(SeparateSource.Static))] + public void Run(int value) { } + } + + // `allows ref struct` needs RuntimeFeature.ByRefLikeGenerics, which .NET Framework does not have, and the + // framework's IEnumerable only took the constraint in .NET 10. The rule itself is not framework-specific; + // only a declaration that exercises it is. +#if NET10_0_OR_GREATER + // The constraint admits a ref struct, but this substitution is not one, so the values read as any other value + // type's do. A substitution that *is* by-ref-like never reaches a validator - discovery names it and throws + // while reading the values, which RefStructSourceTests pins - so judging the declaration here would report + // only the substitutions that work. + [Fact] + public async Task ArgumentsSourceYieldingAParameterAdmittingARefStructIsNotReported() + { + Assert.Empty(await Validate>()); + } + + [Fact] + public async Task ParamsSourceYieldingAParameterAdmittingARefStructIsNotReported() + { + Assert.Empty(await Validate>()); + } + + // The derived type fixes the argument, so the constraint stops deciding anything. + [Fact] + public async Task ASourceClosedByTheDerivedTypeIsNotReported() + { + Assert.Empty(await Validate()); + } + + // One value, so a benchmark case exists for the validator to see. T is never actually a ref struct here - + // reading one is what the rule forbids - so `default` is only ever boxed as the substitution allows. + public class OneValue : IEnumerable, IEnumerator where T : allows ref struct + { + private int index = -1; + + public T Current => default!; + object System.Collections.IEnumerator.Current => null!; + + public IEnumerator GetEnumerator() => new OneValue(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + public bool MoveNext() => ++index == 0; + public void Reset() => index = -1; + public void Dispose() { } + } + + public class AdmitsARefStruct where T : allows ref struct + { + public static IEnumerable Values() => new OneValue(); + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(T value) { } + } + + public class AdmitsARefStructParam where T : allows ref struct + { + public static IEnumerable Values() => new OneValue(); + + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + [Benchmark] public void Run() { } + } + + public class ValuesInAGenericBase where T : allows ref struct + { + public static IEnumerable Values() => new OneValue(); + } + + public class ClosesTheArgument : ValuesInAGenericBase + { + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(int value) { } + } +#endif + + [Fact] + public async Task NonGenericArgumentsSourceIsReported() + { + var messages = await Validate(); + + Assert.Contains(messages, message => message.Contains("neither IEnumerable nor IAsyncEnumerable")); + } + + public class WithGenericEnumerableSource + { + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + public static IEnumerable Values() => [1, 2]; + + [Benchmark] public void Run() { } + } + + public class WithNonGenericEnumerableSource + { + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + public static System.Collections.IEnumerable Values() => new object[] { 1, 2 }; + + [Benchmark] public void Run() { } + } + + public class WithTwoElementTypesSource + { + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + public static TwoElementTypes Values() => new(); + + [Benchmark] public void Run() { } + } + + public class WithTwoAsyncElementTypesSource + { + [ParamsSource(nameof(Values))] + public int Value { get; set; } + + public static TwoAsyncElementTypes Values() => new(); + + [Benchmark] public void Run() { } + } + + public class WithNonGenericArgumentsSource + { + public static System.Collections.IEnumerable Values() => new object[] { 1, 2 }; + + [Benchmark] + [ArgumentsSource(nameof(Values))] + public void Run(int value) { } + } + + public class TwoElementTypes : IEnumerable, IEnumerable + { + IEnumerator IEnumerable.GetEnumerator() => Ints().GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Strings().GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => Ints().GetEnumerator(); + + private static List Ints() => [1, 2]; + private static List Strings() => ["a"]; + } + + public class TwoAsyncElementTypes : IAsyncEnumerable, IAsyncEnumerable + { + IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) + => Ints().GetAsyncEnumerator(cancellationToken); + + IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) + => Strings().GetAsyncEnumerator(cancellationToken); + + private static async IAsyncEnumerable Ints() + { + yield return 1; + await Task.Yield(); + yield return 2; + } + + private static async IAsyncEnumerable Strings() + { + yield return "a"; + await Task.Yield(); + } + } + + [Fact] + public void ABaseTypeStaticParamsSourceIsBoundAndValidated() + { + // A base type's static member is a parameter like any other, so discovery reads it - and reaches this + // source, which is not enumerable, before the validator gets a turn. Binding it at all is the point: + // unbound, it would yield neither a parameter nor a diagnostic. + var exception = Assert.Throws( + () => BenchmarkConverter.TypeToBenchmarks(typeof(InheritsABadStaticParamsSource))); + + Assert.Contains(nameof(BadStaticParamsSourceOnABase.NotASource), exception.Message); + } + + public class BadStaticParamsSourceOnABase + { + // BDN1306 is disabled for the whole file; a local restore here would re-enable it below, since restore + // returns to the project default rather than to the enclosing directive. + [ParamsSource(nameof(NotASource))] + public static int InheritedParameter; + + public static int NotASource() => 0; + } + + public class InheritsABadStaticParamsSource : BadStaticParamsSourceOnABase + { + [Benchmark] + public int Run() => InheritedParameter; + } +} diff --git a/tests/BenchmarkDotNet.Tests/Validators/SynchronizationContextCaptureTests.cs b/tests/BenchmarkDotNet.Tests/Validators/SynchronizationContextCaptureTests.cs new file mode 100644 index 0000000000..0081a3fd6c --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/Validators/SynchronizationContextCaptureTests.cs @@ -0,0 +1,139 @@ +using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Helpers; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Validators; +using System.Collections.Immutable; + +namespace BenchmarkDotNet.Tests.Validators; + +/// +/// BenchmarkDotNet never installs a SynchronizationContext.Current, so whatever is ambient around a run is still +/// ambient inside it. Nothing that drives a suspending sequence may capture it: the continuation would be posted to +/// the caller's context, which may be single-threaded and - while the pump blocks its thread - unable to run it. +/// This is why the composites are written out instead of composed with async LINQ, whose operators decide for +/// themselves whether to capture. +/// +public class SynchronizationContextCaptureTests +{ + [Fact] + public void CompositeValidatorDoesNotCaptureTheAmbientContext() + { + var errors = DrainUnderPump( + () => new CompositeValidator([new SuspendingValidator("first"), new SuspendingValidator("second")]) + .ValidateAsync(Array.Empty())); + + // The validators are held in an ImmutableHashSet, so the order they are visited in is unspecified. + Assert.Equal(["first", "second"], errors.Select(error => error.Message).OrderBy(message => message)); + } + + [Fact] + public void CompositeValidatorDeduplicatesErrors() + { + // The rewrite replaced async LINQ's Distinct(); the behaviour it stood for has to survive. + var errors = DrainUnderPump( + () => new CompositeValidator([new SuspendingValidator("same"), new SuspendingValidator("same")]) + .ValidateAsync(Array.Empty())); + + Assert.Equal(["same"], errors.Select(error => error.Message)); + } + + [Fact] + public void CompositeDiagnoserDoesNotCaptureTheAmbientContext() + { + var errors = DrainUnderPump( + () => new CompositeDiagnoser([new SuspendingDiagnoser("diagnoser")]) + .ValidateAsync(Array.Empty())); + + Assert.Equal(["diagnoser"], errors.Select(error => error.Message)); + } + + // Drives the sequence the way a run does - under BenchmarkDotNet's pump, on a thread carrying an ambient context - + // and asserts the ambient one is never posted to. The pump is what makes this meaningful: without it the pumping + // ConfigureAwait falls back to ConfigureAwait(true) and capturing the caller's context is the intended behaviour. + private static List DrainUnderPump(Func> sequence) + { + var recording = new RecordingSynchronizationContext(); + var original = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(recording); + try + { + using var pump = BenchmarkSynchronizationContext.CreateAndSetCurrent(); + var drained = pump.ExecuteUntilComplete(DrainAsync(sequence())); + + Assert.Equal(0, recording.PostCount); + Assert.Equal(0, recording.SendCount); + return drained; + } + finally + { + SynchronizationContext.SetSynchronizationContext(original); + } + } + + private static async ValueTask> DrainAsync(IAsyncEnumerable errors) + { + var drained = new List(); +#pragma warning disable CA2007 + await foreach (var error in errors.ConfigureAwait()) +#pragma warning restore CA2007 + { + drained.Add(error); + } + return drained; + } + + private sealed class RecordingSynchronizationContext : SynchronizationContext + { + private int postCount; + private int sendCount; + + public int PostCount => Volatile.Read(ref postCount); + public int SendCount => Volatile.Read(ref sendCount); + + public override void Post(SendOrPostCallback d, object? state) + { + Interlocked.Increment(ref postCount); + base.Post(d, state); + } + + public override void Send(SendOrPostCallback d, object? state) + { + Interlocked.Increment(ref sendCount); + base.Send(d, state); + } + } + + // Suspends before yielding, and configures its own await away so only the consumer's machinery could capture. + private sealed class SuspendingValidator(string message) : IValidator + { + public bool TreatsWarningsAsErrors => true; + + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) + { + await Task.Delay(1).ConfigureAwait(false); + yield return new ValidationError(TreatsWarningsAsErrors, message); + } + } + + private sealed class SuspendingDiagnoser(string message) : IDiagnoser + { + public IEnumerable Ids => [message]; + public IEnumerable Exporters => []; + public IEnumerable Analysers => []; + public RunMode GetRunMode(BenchmarkCase benchmarkCase) => RunMode.None; + public ValueTask HandleAsync(HostSignal signal, DiagnoserActionParameters parameters, CancellationToken cancellationToken) => default; + public IEnumerable ProcessResults(DiagnoserResults results) => []; + public void DisplayResults(ILogger logger) { } + + public async IAsyncEnumerable ValidateAsync(ValidationParameters validationParameters) + { + await Task.Delay(1).ConfigureAwait(false); + yield return new ValidationError(true, message); + } + } +}