Skip to content

Commit

Permalink
Add Mock.Of<T> and MockRepository support to ConstructorArgumentsShou…
Browse files Browse the repository at this point in the history
…ldMatchAnalyzer (#140)

The majority of this change is mechanical (moving code from one location
to another, replacing magic strings with constants, etc.). There is a
functional update that completely changes the
`ConstructorArgumentsShouldMatchAnalyzer`:

- consolidating two constructor analyzers now into a single shared
source
- improving the performance of the constructor analyzer
- adding support for checking delegate
- adding support for `Mock.Of<T>` and `MockRepository` features of `Moq`


![image](https://github.com/rjmurillo/moq.analyzers/assets/6811113/cf6e2e52-237c-4f5e-bdc8-dadb0026eeba)
_Baseline is an empty analyzer, control is the old interface constructor
parameter analyzer, treatment is the new combined analyzer._

Changes

Misc:
- Reduced severity of AV analyzer items (see #139)
- Move magic strings to constants
- Moved much of the common code into a `Common` folder and namespace

Documentation:
- Fixed links to documentation for shipped analyzers
- In analyzer rule help, link to commit id so documentation snaps to the
version of the analyzer

Analyzers:
- Moved rule ID to constants file
- Added extension factory methods to help create `Diagnostic`
- Consolidated two constructor parameter analyzers (Moq1001 and Moq1002)
into a single analyzer with 30% better performance
- `ConstructorArgumentsShouldMatchAnalyzer` now supports `Mock.Of<T>`
syntax and `MockRepository` patterns

Benchmarks:
- Add the ability to have a control and treatment as well as "baseline",
allowing A/B performance benchmarking of implementations
- Added benchmark for `ConstructorArgumentsShouldMatchAnalyzer`

Test:
- Added extension methods to include all Moq versions under test or just
the latest
- Organized the `ConstructorArgumentsShouldMatchAnalyzer` test
collateral (it's the highest volume) into partial files
- Added Live Unit Test configuration when running in Visual Studio
- Added code coverage filter for benchmark code if using ReSharper or
Rider

_Note: while code coverage is improved, it's still below 95%. This is
due to some guard cases that need coverage. For example, #141_

Resolves #122
For Moq1001 and Moq1002, #85
  • Loading branch information
rjmurillo authored Jul 14, 2024
1 parent cc48498 commit 1d1533a
Show file tree
Hide file tree
Showing 49 changed files with 1,355 additions and 492 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -401,4 +401,4 @@ dotnet_diagnostic.CA2016.severity = error
# S1135: Track uses of "TODO" tags
dotnet_diagnostic.S1135.severity = suggestion
# MA0026: Fix TODO comment
dotnet_diagnostic.MA0026.severity = none
dotnet_diagnostic.MA0026.severity = none
153 changes: 153 additions & 0 deletions .lutignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
## The .lutignore file is used by Live Unit Testing to ignore Visual Studio temporary files, build results,
## and files generated by popular Visual Studio add-ons when creating a private copy of the source tree that
## Live Unit Testing uses for its build.
##
## This file has same format as git's .gitignore file (https://git-scm.com/docs/gitignore). In fact, in the
## case where a .lutignore file is not found, but a .gitignore file is found, Live Unit Testing will use the
## .gitignore file directly for the above purpose.

# User-specific files
*.suo
*.user
*.userprefs
*.sln.docstates
.vs/
.vscode/
.packages/
.dotnet/
.tools/
.idea/

# Build results
[Dd]ebug/
[Rr]elease/
[Bb]inaries/
[Bb]in/
[Oo]bj/
x64/
TestResults/

# Debug artifactss
launchSettings.json

# Click-Once directory
publish/

# Publish Web Output
*.Publish.xml

# NuGet Packages Directory
packages/

# NuGet V3 artifacts
[Nn]u[Gg]et.exe
*-packages.config
*.nuget.props
*.nuget.targets
project.lock.json
msbuild.binlog
*.project.lock.json

# Miscellaneous
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.wrn
*.vspscc
*.vssscc
.builds
*.pidb
*.scc
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.pfx
*.publishsettings

# Visual Studio cache files
*.sln.ide/

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
*.VC.opendb
*.VC.db

# Windows Store app package directory
AppPackages/

# Visual Studio profiler
*.psess
*.vsp
*.vspx

# Guidance Automation Toolkit
*.gpState

# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# NCrunch
*.ncrunch*
.*crunch*.local.xml

# Upgrade backups
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm

# SQL Server files
App_Data/*.mdf
App_Data/*.ldf

#LightSwitch generated files
GeneratedArtifacts/
_Pvt_Extensions/
ModelManifest.xml

# Windows image file caches
Thumbs.db
ehthumbs.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Mac desktop service store files
.DS_Store

# WPF temp projects
*wpftmp.*
6 changes: 6 additions & 0 deletions Moq.Analyzers.lutconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<LUTConfig Version="1.0">
<Repository />
<ParallelBuilds>true</ParallelBuilds>
<ParallelTestRuns>true</ParallelTestRuns>
<TestCaseTimeout>180000</TestCaseTimeout>
</LUTConfig>
2 changes: 2 additions & 0 deletions Moq.Analyzers.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/Environment/Filtering/ExcludeCoverageFilters/=Moq_002EAnalyzers_002EBenchmarks_003B_002A_003B_002A_003B_002A/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
18 changes: 9 additions & 9 deletions src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
Moq1101 | Moq | Warning | CallbackSignatureAnalyzer
Moq1002 | Moq | Warning | ShouldNotAllowParametersForMockedInterfaceAnalyzer
Moq1001 | Moq | Warning | ShouldNotMockSealedClassesAnalyzer
Moq1101 | Moq | Warning | CallbackSignatureAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1101.md)
Moq1002 | Moq | Warning | ShouldNotAllowParametersForMockedInterfaceAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1002.md)
Moq1001 | Moq | Warning | ShouldNotMockSealedClassesAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1001.md)

## Release 0.0.3.40797

Expand All @@ -25,37 +25,37 @@ Moq1003 | Moq | Warning | MatchingConstructorParametersAnalyzer

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
Moq1100 | Moq | Warning | CallbackSignatureShouldMatchMockedMethodAnalyzer
Moq1000 | Moq | Warning | ConstructorArgumentsShouldMatchAnalyzer
Moq1100 | Moq | Warning | CallbackSignatureShouldMatchMockedMethodAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1100.md)
Moq1000 | Moq | Warning | ConstructorArgumentsShouldMatchAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1000.md)

## Release 0.0.6

### Removed Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
Moq1003 | Moq | Warning | ConstructorArgumentsShouldMatchAnalyzer
Moq1003 | Moq | Warning | ConstructorArgumentsShouldMatchAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1003.md)

## Release 0.0.7

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
Moq1200 | Moq | Error | SetupShouldBeUsedOnlyForOverridableMembersAnalyzer
Moq1200 | Moq | Error | SetupShouldBeUsedOnlyForOverridableMembersAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1200.md)

## Release 0.0.8

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
Moq1300 | Moq | Error | AsShouldBeUsedOnlyForInterfaceAnalyzer
Moq1300 | Moq | Error | AsShouldBeUsedOnlyForInterfaceAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1300.md)

## Release 0.0.9

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
Moq1201 | Moq | Error | SetupShouldNotIncludeAsyncResultAnalyzer
Moq1201 | Moq | Error | SetupShouldNotIncludeAsyncResultAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1201.md)
54 changes: 29 additions & 25 deletions src/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,57 @@ namespace Moq.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsShouldBeUsedOnlyForInterfaceAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "Moq1300";
private const string Title = "Moq: Invalid As type parameter";
private const string Message = "Mock.As() should take interfaces only";

private static readonly DiagnosticDescriptor Rule = new(
RuleId,
DiagnosticIds.AsShouldOnlyBeUsedForInterfacesRuleId,
Title,
Message,
DiagnosticCategory.Moq,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/{RuleId}.md");
helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/{ThisAssembly.GitCommitId}/docs/rules/{DiagnosticIds.AsShouldOnlyBeUsedForInterfacesRuleId}.md");

/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);

/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(RegisterCompilationStartAction);
}

context.RegisterCompilationStartAction(static context =>
private void RegisterCompilationStartAction(CompilationStartAnalysisContext context)
{
// Ensure Moq is referenced in the compilation
ImmutableArray<INamedTypeSymbol> mockTypes = context.Compilation.GetMoqMock();
if (mockTypes.IsEmpty)
{
// Ensure Moq is referenced in the compilation
ImmutableArray<INamedTypeSymbol> mockTypes = context.Compilation.GetMoqMock();
if (mockTypes.IsEmpty)
{
return;
}
return;
}

// Look for the Mock.As() method and provide it to Analyze to avoid looking it up multiple times.
ImmutableArray<IMethodSymbol> asMethods = mockTypes
.SelectMany(mockType => mockType.GetMembers(WellKnownTypeNames.As))
.OfType<IMethodSymbol>()
.Where(method => method.IsGenericMethod)
.ToImmutableArray();

// Look for the Mock.As() method and provide it to Analyze to avoid looking it up multiple times.
ImmutableArray<IMethodSymbol> asMethods = mockTypes
.SelectMany(mockType => mockType.GetMembers("As"))
.OfType<IMethodSymbol>()
.Where(method => method.IsGenericMethod)
.ToImmutableArray();
if (asMethods.IsEmpty)
{
return;
}
if (asMethods.IsEmpty)
{
return;
}

context.RegisterOperationAction(context => Analyze(context, asMethods), OperationKind.Invocation);
});
context.RegisterOperationAction(
operationAnalysisContext => Analyze(operationAnalysisContext, asMethods),
OperationKind.Invocation);
}

private static void Analyze(OperationAnalysisContext context, ImmutableArray<IMethodSymbol> wellKnownAsMethods)
private void Analyze(OperationAnalysisContext context, ImmutableArray<IMethodSymbol> wellKnownAsMethods)
{
if (context.Operation is not IInvocationOperation invocationOperation)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Diagnostics;

namespace Moq.Analyzers;

/// <summary>
Expand All @@ -8,7 +6,7 @@ namespace Moq.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CallbackSignatureShouldMatchMockedMethodAnalyzer : DiagnosticAnalyzer
{
internal const string RuleId = "Moq1100";
internal const string RuleId = DiagnosticIds.BadCallbackParameters;
private const string Title = "Moq: Bad callback parameters";
private const string Message = "Callback signature must match the signature of the mocked method";

Expand All @@ -19,7 +17,7 @@ public class CallbackSignatureShouldMatchMockedMethodAnalyzer : DiagnosticAnalyz
DiagnosticCategory.Moq,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/{RuleId}.md");
helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/{ThisAssembly.GitCommitId}/docs/rules/{RuleId}.md");

/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Microsoft.CodeAnalysis.Operations;

namespace Moq.Analyzers;
namespace Moq.Analyzers.Common;

internal static class CompilationExtensions
{
Expand All @@ -27,7 +25,7 @@ public static ImmutableArray<INamedTypeSymbol> GetTypesByMetadataNames(this Comp
}

/// <summary>
/// Get the Moq.Mock and Moq.Mock`1 type symbols (if part of the compilation).
/// Get the Moq.MockRepository, Moq.Mock and Moq.Mock`1 type symbols (if part of the compilation).
/// </summary>
/// <param name="compilation">The <see cref="Compilation"/> to inspect.</param>
/// <returns>
Expand All @@ -36,6 +34,6 @@ public static ImmutableArray<INamedTypeSymbol> GetTypesByMetadataNames(this Comp
/// </returns>
public static ImmutableArray<INamedTypeSymbol> GetMoqMock(this Compilation compilation)
{
return compilation.GetTypesByMetadataNames([WellKnownTypeNames.MoqMock, WellKnownTypeNames.MoqMock1]);
return compilation.GetTypesByMetadataNames([WellKnownTypeNames.MoqMock, WellKnownTypeNames.MoqMock1, WellKnownTypeNames.MoqRepository]);
}
}
21 changes: 21 additions & 0 deletions src/Moq.Analyzers/Common/CompilationOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Moq.Analyzers.Common;

internal static class CompilationOptionsExtensions
{
/// <summary>
/// Determines if the diagnostic identified by the given identifier is currently suppressed.
/// </summary>
/// <param name="compilationOptions">The compilation options that will be used to determine if the diagnostic is currently suppressed.</param>
/// <param name="descriptor">The diagnostic descriptor to check.</param>
/// <returns>True if the diagnostic is currently suppressed.</returns>
internal static bool IsAnalyzerSuppressed(this CompilationOptions compilationOptions, DiagnosticDescriptor descriptor)
{
switch (descriptor.GetEffectiveSeverity(compilationOptions))
{
case ReportDiagnostic.Suppress:
return true;
default:
return false;
}
}
}
6 changes: 6 additions & 0 deletions src/Moq.Analyzers/Common/DiagnosticCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Moq.Analyzers.Common;

internal static class DiagnosticCategory
{
internal const string Moq = nameof(Moq);
}
Loading

0 comments on commit 1d1533a

Please sign in to comment.