Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Moq.Analyzers.Benchmarks with a single sample benchmark #109

Merged
merged 13 commits into from
Jun 21, 2024
7 changes: 7 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
# SA1309: Field names must not begin with underscore
# Keep this aligned with `dotnet_naming_rule.private_fields_should_be__camelcase.style`
dotnet_diagnostic.SA1309.severity = none

dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
Expand Down Expand Up @@ -398,6 +401,10 @@ dotnet_diagnostic.MA0040.severity = error
# Async analyzer
dotnet_diagnostic.CA2016.severity = error

# AV1555: Avoid using named arguments
# Disabled because it's common to use a named argument when passing `null` or bool arguments to make the parameter's purpose clear
dotnet_diagnostic.AV1555.severity = none

#### Handling TODOs ####
# This is a popular rule in analyzers. Everyone has an opinion and
# some of the severity levels conflict. We don't need all of these
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="GetPackFromProject" Version="1.0.6" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.139" />
</ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers", "src\Moq.An
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Test", "tests\Moq.Analyzers.Test\Moq.Analyzers.Test.csproj", "{D2348836-7129-4BE5-8AE6-D05FC8C28FC1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Benchmarks", "tests\Moq.Analyzers.Benchmarks\Moq.Analyzers.Benchmarks.csproj", "{11B3412F-456C-452E-94D2-B42D5C52F61C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,6 +23,10 @@ Global
{D2348836-7129-4BE5-8AE6-D05FC8C28FC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2348836-7129-4BE5-8AE6-D05FC8C28FC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2348836-7129-4BE5-8AE6-D05FC8C28FC1}.Release|Any CPU.Build.0 = Release|Any CPU
{11B3412F-456C-452E-94D2-B42D5C52F61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11B3412F-456C-452E-94D2-B42D5C52F61C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11B3412F-456C-452E-94D2-B42D5C52F61C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11B3412F-456C-452E-94D2-B42D5C52F61C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
9 changes: 9 additions & 0 deletions build/targets/codeanalysis/.globalconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
is_global=true

# Prefer configuring diagnostics in .editorconfig over this .globalconfig because it is understood by more editors.
# Only use this file for configuring diagnostics that aren't tied to a source file, and thus can't be placed under
# any .editorconfig section.

# AV2210 : Pass -warnaserror to the compiler or add <TreatWarningsAsErrors>True</TreatWarningsAsErrors> to your project file
# This is set as part of the CI build. It is intentionally not set locally to allow for a fast inner dev loop.
dotnet_diagnostic.AV2210.severity = none
1 change: 1 addition & 0 deletions build/targets/codeanalysis/CodeAnalysis.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" Visible="false" />
<GlobalAnalyzerConfigFiles Include="$(MSBuildThisFileDirectory)/.globalconfig" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ dotnet_diagnostic.CS1591.severity = suggestion
dotnet_diagnostic.CS1712.severity = suggestion

# VSTHRD200: Use "Async" suffix for async methods
# AV1755: Postfix asynchronous methods with Async or TaskAsync
# Just about every test method is async, doesn't provide any real value and clustters up test window
dotnet_diagnostic.VSTHRD200.severity = none
dotnet_diagnostic.VSTHRD200.severity = none
dotnet_diagnostic.AV1755.severity = none
6 changes: 6 additions & 0 deletions tests/Moq.Analyzers.Benchmarks/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Moq.Analyzers.Benchmarks;

internal static class Constants
{
public const int NumberOfCodeFiles = 1_000;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.CodeAnalysis.Diagnostics;

namespace Moq.Analyzers.Benchmarks.Helpers;

internal static class AnalysisResultExtensions
{
public static AnalysisResult AssertValidAnalysisResult(this AnalysisResult analysisResult)
{
if (analysisResult.Analyzers.Length != 1)
{
throw new InvalidOperationException($"Expected a single analyzer but found '{analysisResult.Analyzers.Length}'");
}

if (analysisResult.CompilationDiagnostics.Count != 0)
{
throw new InvalidOperationException($"Expected no compilation diagnostics but found '{analysisResult.CompilationDiagnostics.Count}'");
}

return analysisResult;
}
}
46 changes: 46 additions & 0 deletions tests/Moq.Analyzers.Benchmarks/Helpers/AsyncLazy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Runtime.CompilerServices;

namespace Moq.Analyzers.Benchmarks.Helpers;

internal class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(
Func<T> valueFactory,
CancellationToken cancellationToken,
TaskScheduler? scheduler = null,
TaskCreationOptions taskCreationOptions = TaskCreationOptions.None,
LazyThreadSafetyMode mode = LazyThreadSafetyMode.ExecutionAndPublication)
: base(
() =>
Task.Factory.StartNew(
valueFactory,
cancellationToken,
taskCreationOptions,
scheduler ?? TaskScheduler.Default),
mode)
{
}

public AsyncLazy(
Func<Task<T>> taskFactory,
CancellationToken cancellationToken,
TaskScheduler? scheduler = null,
TaskCreationOptions taskCreationOptions = TaskCreationOptions.None,
LazyThreadSafetyMode mode = LazyThreadSafetyMode.ExecutionAndPublication)
: base(
() =>
Task.Factory.StartNew(
() => taskFactory(),
cancellationToken,
taskCreationOptions,
scheduler ?? TaskScheduler.Default)
.Unwrap(),
mode)
{
}

public TaskAwaiter<T> GetAwaiter()
{
return Value.GetAwaiter();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;

namespace Moq.Analyzers.Benchmarks.Helpers;

internal static class BenchmarkCSharpCompilationCreator<TAnalyzer>
where TAnalyzer : DiagnosticAnalyzer, new()
{
public static async Task<(CompilationWithAnalyzers Baseline, CompilationWithAnalyzers Test)> CreateAsync(
(string Name, string Contents)[] sources,
AnalyzerOptions? options = null)
{
Compilation? compilation = await CSharpCompilationCreator.CreateAsync(sources).ConfigureAwait(false);

if (compilation is null)
{
throw new InvalidOperationException("Failed to create compilation");
}

CompilationWithAnalyzers baseline = compilation.WithAnalyzers([new EmptyDiagnosticAnalyzer()], options, CancellationToken.None);
CompilationWithAnalyzers test = compilation.WithAnalyzers([new TAnalyzer()], options, CancellationToken.None);

return (baseline, test);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Moq.Analyzers.Benchmarks.Helpers;

namespace Moq.Analyzers.Benchmarks;

// Originally from https://github.com/dotnet/roslyn-analyzers/blob/f1115edce8633ebe03a86191bc05c6969ed9a821/src/PerformanceTests/Utilities/CSharp/CSharpCompilationHelper.cs
// See https://github.com/dotnet/roslyn-sdk/issues/1165 for discussion on providing these or similar helpers in the testing packages.
internal static class CSharpCompilationCreator
{
public static async Task<Compilation?> CreateAsync((string, string)[] sourceFiles)
{
(Project project, _) = await CreateProjectAsync(sourceFiles, globalOptions: null).ConfigureAwait(false);
return await project.GetCompilationAsync().ConfigureAwait(false);
}

public static async Task<(Compilation? Compilation, AnalyzerOptions Options)> CreateWithOptionsAsync((string, string)[] sourceFiles, (string, string)[] globalOptions)
{
(Project project, AnalyzerOptions options) = await CreateProjectAsync(sourceFiles, globalOptions).ConfigureAwait(false);
return (await project.GetCompilationAsync().ConfigureAwait(false), options);
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "AV1553:Do not use optional parameters with default value null for strings, collections or tasks", Justification = "Minimizing divergence from upstream code")]
private static Task<(Project Project, AnalyzerOptions Options)> CreateProjectAsync((string, string)[] sourceFiles, (string, string)[]? globalOptions = null)
=> CompilationCreator.CreateProjectAsync(
sourceFiles,
globalOptions,
"TestProject",
LanguageNames.CSharp,
"/0/Test",
"cs",
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary),
new CSharpParseOptions(LanguageVersion.Default));
}
159 changes: 159 additions & 0 deletions tests/Moq.Analyzers.Benchmarks/Helpers/CompilationCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Model;
using Microsoft.VisualStudio.Composition;

namespace Moq.Analyzers.Benchmarks.Helpers;

// Originally from https://github.com/dotnet/roslyn-analyzers/blob/f1115edce8633ebe03a86191bc05c6969ed9a821/src/PerformanceTests/Utilities/Common/CompilationHelper.cs
// See https://github.com/dotnet/roslyn-sdk/issues/1165 for discussion on providing these or similar helpers in the testing packages.
internal static class CompilationCreator
{
private static readonly ReferenceAssemblies ReferenceAssemblies = ReferenceAssemblies.Net.Net80.AddPackages([new PackageIdentity("Moq", "4.18.4")]);

[SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Minimizing divergence from upstream code.")]
public static async Task<(Project Project, AnalyzerOptions Options)> CreateProjectAsync(
(string, string)[] sourceFiles,
(string, string)[]? globalOptions,
string name,
string language,
string defaultPrefix,
string defaultExtension,
CompilationOptions compilationOptions,
ParseOptions parseOptions)
{
ProjectState projectState = new ProjectState(name, language, defaultPrefix, defaultExtension);
foreach ((string filename, string content) in sourceFiles)
{
projectState.Sources.Add((defaultPrefix + filename + "." + defaultExtension, content));
}

EvaluatedProjectState evaluatedProj = new EvaluatedProjectState(projectState, ReferenceAssemblies);

Project project = await CreateProjectAsync(evaluatedProj, compilationOptions, parseOptions).ConfigureAwait(false);

if (globalOptions is not null)
{
OptionsProvider optionsProvider = new(globalOptions);
AnalyzerOptions options = new(ImmutableArray<AdditionalText>.Empty, optionsProvider);

return (project, options);
}

return (project, project.AnalyzerOptions);
}

[SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Minimizing divergence with upstream code")]
[SuppressMessage("Maintainability", "AV1551:Method overload should call another overload", Justification = "Minimizing divergence with upstream code")]
[SuppressMessage("Maintainability", "AV1555:Avoid using non-(nullable-)boolean named arguments", Justification = "Minimizing divergence with upstream code")]
private static async Task<Project> CreateProjectAsync(
EvaluatedProjectState primaryProject,
CompilationOptions compilationOptions,
ParseOptions parseOptions)
{
ProjectId projectId = ProjectId.CreateNewId(debugName: primaryProject.Name);
Solution solution = await CreateSolutionAsync(projectId, primaryProject, compilationOptions, parseOptions).ConfigureAwait(false);

foreach ((string newFileName, Microsoft.CodeAnalysis.Text.SourceText source) in primaryProject.Sources)
{
DocumentId documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
solution = solution.AddDocument(documentId, newFileName, source, filePath: newFileName);
}

foreach ((string newFileName, Microsoft.CodeAnalysis.Text.SourceText source) in primaryProject.AdditionalFiles)
{
DocumentId documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
solution = solution.AddAdditionalDocument(documentId, newFileName, source, filePath: newFileName);
}

foreach ((string newFileName, Microsoft.CodeAnalysis.Text.SourceText source) in primaryProject.AnalyzerConfigFiles)
{
DocumentId documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
solution = solution.AddAnalyzerConfigDocument(documentId, newFileName, source, filePath: newFileName);
}

return solution.GetProject(projectId)!;
}

[SuppressMessage("Maintainability", "AV1500:Member or local function contains too many statements", Justification = "Minimizing divergence from upstream")]
[SuppressMessage("Maintainability", "AV1561:Signature contains too many parameters", Justification = "Minimizing divergence from upstream")]
private static async Task<Solution> CreateSolutionAsync(
ProjectId projectId,
EvaluatedProjectState projectState,
CompilationOptions compilationOptions,
ParseOptions parseOptions)
{
ReferenceAssemblies referenceAssemblies = projectState.ReferenceAssemblies ?? ReferenceAssemblies.Default;

compilationOptions = compilationOptions
.WithOutputKind(projectState.OutputKind)
.WithAssemblyIdentityComparer(referenceAssemblies.AssemblyIdentityComparer);

parseOptions = parseOptions
.WithDocumentationMode(projectState.DocumentationMode);

AsyncLazy<IExportProviderFactory> exportProviderFactory = new(
async () =>
{
AttributedPartDiscovery discovery = new(Resolver.DefaultInstance, isNonPublicSupported: true);
DiscoveredParts parts = await discovery.CreatePartsAsync(MefHostServices.DefaultAssemblies).ConfigureAwait(false);
ComposableCatalog catalog = ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts);

CompositionConfiguration configuration = CompositionConfiguration.Create(catalog);
RuntimeComposition runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
return runtimeComposition.CreateExportProviderFactory();
},
CancellationToken.None);
ExportProvider exportProvider = (await exportProviderFactory).CreateExportProvider();
MefHostServices host = MefHostServices.Create(exportProvider.AsCompositionContext());
AdhocWorkspace workspace = new AdhocWorkspace(host);

Solution solution = workspace
.CurrentSolution
.AddProject(projectId, projectState.Name, projectState.Name, projectState.Language)
.WithProjectCompilationOptions(projectId, compilationOptions)
.WithProjectParseOptions(projectId, parseOptions);

ImmutableArray<MetadataReference> metadataReferences = await referenceAssemblies.ResolveAsync(projectState.Language, CancellationToken.None).ConfigureAwait(false);
solution = solution.AddMetadataReferences(projectId, metadataReferences);

return solution;
}

/// <summary>
/// This class just passes argument through to the projects options provider and it used to provider custom global options.
/// </summary>
private sealed class OptionsProvider : AnalyzerConfigOptionsProvider
{
public OptionsProvider((string, string)[] globalOptions)
{
GlobalOptions = new ConfigOptions(globalOptions);
}

public override AnalyzerConfigOptions GlobalOptions { get; }

public override AnalyzerConfigOptions GetOptions(SyntaxTree tree)
=> GlobalOptions;

public override AnalyzerConfigOptions GetOptions(AdditionalText textFile)
=> GlobalOptions;
}

/// <summary>
/// Allows adding additional global options.
/// </summary>
private sealed class ConfigOptions : AnalyzerConfigOptions
{
private readonly Dictionary<string, string> _globalOptions;

public ConfigOptions((string, string)[] globalOptions)
=> _globalOptions = globalOptions.ToDictionary(kvp => kvp.Item1, kvp => kvp.Item2, StringComparer.OrdinalIgnoreCase);

public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
=> _globalOptions.TryGetValue(key, out value);
}
}
Loading
Loading