diff --git a/docs/rules/Moq1410.md b/docs/rules/Moq1410.md new file mode 100644 index 0000000..cfd4e56 --- /dev/null +++ b/docs/rules/Moq1410.md @@ -0,0 +1,74 @@ +# Moq1410: Explicitly set the Strict mocking behavior + +# Moq1410: Explicitly set the Strict mocking behavior + +| Item | Value | +| -------- | ------- | +| Enabled | True | +| Severity | Info | +| CodeFix | True | + +--- + +Mocks use the `MockBehavior.Loose` by default. Some people find this default behavior undesirable, as it can lead to +unexpected behavior if the mock is improperly set up. To fix, specify `MockBehavior.Strict` to cause Moq to always throw +an exception for invocations that don't have a corresponding setup. + +## Examples of patterns that are flagged by this analyzer + +```csharp +interface ISample +{ + int Calculate() => 0; +} + +var mock = new Mock(); // Moq1410: Moq: Explicitly set the Strict mocking behavior +var mock2 = Mock.Of(); // Moq1410: Moq: Explicitly set the Strict mocking behavior +``` + +```csharp +interface ISample +{ + int Calculate() => 0; +} + +var mock = new Mock(MockBehavior.Default); // Moq1410: Explicitly set the Strict mocking behavior +var mock2 = Mock.Of(MockBehavior.Default); // Moq1410: Explicitly set the Strict mocking behavior +var repo = new MockRepository(MockBehavior.Default); // Moq1410: Explicitly set the Strict mocking behavior +``` + +## Solution + +```csharp +interface ISample +{ + int Calculate() => 0; +} + +var mock = new Mock(MockBehavior.Strict); +var mock2 = Mock.Of(MockBehavior.Strict); +var repo = new MockRepository(MockBehavior.Strict); +``` + +## Suppress a warning + +If you just want to suppress a single violation, add preprocessor directives to +your source file to disable and then re-enable the rule. + +```csharp +#pragma warning disable Moq1410 +var mock = new Mock(); // Moq1410: Moq: Explicitly set the Strict mocking behavior +#pragma warning restore Moq1410 +``` + +To disable the rule for a file, folder, or project, set its severity to `none` +in the +[configuration file](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files). + +```ini +[*.{cs,vb}] +dotnet_diagnostic.Moq1410.severity = none +``` + +For more information, see +[How to suppress code analysis warnings](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings). diff --git a/docs/rules/README.md b/docs/rules/README.md index 2269cbe..e41c062 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -11,3 +11,4 @@ | [Moq1201](./Moq1201.md) | Setup of async methods should use `.ReturnsAsync` instance instead of `.Result` | | [Moq1300](./Moq1300.md) | `Mock.As()` should take interfaces only | | [Moq1400](./Moq1400.md) | Explicitly choose a mocking behavior instead of relying on the default (Loose) behavior | +| [Moq1400](./Moq1410.md) | Explicitly set the Strict mocking behavior | diff --git a/src/Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/AnalyzerReleases.Unshipped.md index fbe594b..30cf425 100644 --- a/src/Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/AnalyzerReleases.Unshipped.md @@ -5,4 +5,5 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -Moq1400 | Moq | Warning | SetExplicitMockBehaviorAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1400.md) \ No newline at end of file +Moq1400 | Moq | Warning | SetExplicitMockBehaviorAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1400.md) +Moq1410 | Moq | Info | SetStrictMockBehaviorAnalyzer, [Documentation](https://github.com/rjmurillo/moq.analyzers/blob/main/docs/rules/Moq1410.md) diff --git a/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs b/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs new file mode 100644 index 0000000..bbd2a46 --- /dev/null +++ b/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs @@ -0,0 +1,81 @@ +using Microsoft.CodeAnalysis.Operations; + +namespace Moq.Analyzers; + +/// +/// Serves as a base class for diagnostic analyzers that analyze mock behavior in Moq. +/// +/// +/// This abstract class provides common functionality for analyzing Moq's MockBehavior, such as registering +/// compilation start actions and defining the core analysis logic to be implemented by derived classes. +/// +public abstract class MockBehaviorDiagnosticAnalyzerBase : DiagnosticAnalyzer +{ + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(RegisterCompilationStartAction); + } + + internal abstract void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols); + + private void RegisterCompilationStartAction(CompilationStartAnalysisContext context) + { + MoqKnownSymbols knownSymbols = new(context.Compilation); + + // Ensure Moq is referenced in the compilation + if (!knownSymbols.IsMockReferenced()) + { + return; + } + + // Look for the MockBehavior type and provide it to Analyze to avoid looking it up multiple times. + if (knownSymbols.MockBehavior is null) + { + return; + } + + context.RegisterOperationAction(context => AnalyzeObjectCreation(context, knownSymbols), OperationKind.ObjectCreation); + + context.RegisterOperationAction(context => AnalyzeInvocation(context, knownSymbols), OperationKind.Invocation); + } + + private void AnalyzeObjectCreation(OperationAnalysisContext context, MoqKnownSymbols knownSymbols) + { + if (context.Operation is not IObjectCreationOperation creation) + { + return; + } + + if (creation.Type is null || + creation.Constructor is null || + !(creation.Type.IsInstanceOf(knownSymbols.Mock1) || creation.Type.IsInstanceOf(knownSymbols.MockRepository))) + { + // We could expand this check to include any method that accepts a MockBehavior parameter. + // Leaving it narrowly scoped for now to avoid false positives and potential performance problems. + return; + } + + AnalyzeCore(context, creation.Constructor, creation.Arguments, knownSymbols); + } + + private void AnalyzeInvocation(OperationAnalysisContext context, MoqKnownSymbols knownSymbols) + { + if (context.Operation is not IInvocationOperation invocation) + { + return; + } + + if (!invocation.TargetMethod.IsInstanceOf(knownSymbols.MockOf, out IMethodSymbol? match)) + { + // We could expand this check to include any method that accepts a MockBehavior parameter. + // Leaving it narrowly scoped for now to avoid false positives and potential performance problems. + return; + } + + AnalyzeCore(context, match, invocation.Arguments, knownSymbols); + } +} diff --git a/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs b/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs index 3ba6a61..ba229cd 100644 --- a/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs +++ b/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs @@ -7,7 +7,7 @@ namespace Moq.Analyzers; /// Mock should explicitly specify a behavior and not rely on the default. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] -public class SetExplicitMockBehaviorAnalyzer : DiagnosticAnalyzer +public class SetExplicitMockBehaviorAnalyzer : MockBehaviorDiagnosticAnalyzerBase { private static readonly LocalizableString Title = "Moq: Explicitly choose a mock behavior"; private static readonly LocalizableString Message = "Explicitly choose a mocking behavior instead of relying on the default (Loose) behavior"; @@ -25,73 +25,8 @@ public class SetExplicitMockBehaviorAnalyzer : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); /// - public override void Initialize(AnalysisContext context) - { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - - context.RegisterCompilationStartAction(RegisterCompilationStartAction); - } - - private static void RegisterCompilationStartAction(CompilationStartAnalysisContext context) - { - MoqKnownSymbols knownSymbols = new(context.Compilation); - - // Ensure Moq is referenced in the compilation - if (!knownSymbols.IsMockReferenced()) - { - return; - } - - // Look for the MockBehavior type and provide it to Analyze to avoid looking it up multiple times. - if (knownSymbols.MockBehavior is null) - { - return; - } - - context.RegisterOperationAction(context => AnalyzeObjectCreation(context, knownSymbols), OperationKind.ObjectCreation); - - context.RegisterOperationAction(context => AnalyzeInvocation(context, knownSymbols), OperationKind.Invocation); - } - - private static void AnalyzeObjectCreation(OperationAnalysisContext context, MoqKnownSymbols knownSymbols) - { - if (context.Operation is not IObjectCreationOperation creation) - { - return; - } - - if (creation.Type is null || - creation.Constructor is null || - !(creation.Type.IsInstanceOf(knownSymbols.Mock1) || creation.Type.IsInstanceOf(knownSymbols.MockRepository))) - { - // We could expand this check to include any method that accepts a MockBehavior parameter. - // Leaving it narrowly scoped for now to avoid false positives and potential performance problems. - return; - } - - AnalyzeCore(context, creation.Constructor, creation.Arguments, knownSymbols); - } - - private static void AnalyzeInvocation(OperationAnalysisContext context, MoqKnownSymbols knownSymbols) - { - if (context.Operation is not IInvocationOperation invocation) - { - return; - } - - if (!invocation.TargetMethod.IsInstanceOf(knownSymbols.MockOf, out IMethodSymbol? match)) - { - // We could expand this check to include any method that accepts a MockBehavior parameter. - // Leaving it narrowly scoped for now to avoid false positives and potential performance problems. - return; - } - - AnalyzeCore(context, match, invocation.Arguments, knownSymbols); - } - [SuppressMessage("Design", "MA0051:Method is too long", Justification = "Should be fixed. Ignoring for now to avoid additional churn as part of larger refactor.")] - private static void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols) + internal override void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols) { // Check if the target method has a parameter of type MockBehavior IParameterSymbol? mockParameter = target.Parameters.DefaultIfNotSingle(parameter => parameter.Type.IsInstanceOf(knownSymbols.MockBehavior)); diff --git a/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs b/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs new file mode 100644 index 0000000..d737eaf --- /dev/null +++ b/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs @@ -0,0 +1,93 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; + +namespace Moq.Analyzers; + +/// +/// Mock should explicitly specify Strict behavior. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class SetStrictMockBehaviorAnalyzer : MockBehaviorDiagnosticAnalyzerBase +{ + private static readonly LocalizableString Title = "Moq: Set MockBehavior to Strict"; + private static readonly LocalizableString Message = "Explicitly set the Strict mocking behavior"; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticIds.SetStrictMockBehavior, + Title, + Message, + DiagnosticCategory.Moq, + DiagnosticSeverity.Info, + isEnabledByDefault: true, + helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/{ThisAssembly.GitCommitId}/docs/rules/{DiagnosticIds.SetStrictMockBehavior}.md"); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + /// + [SuppressMessage("Design", "MA0051:Method is too long", Justification = "Should be fixed. Ignoring for now to avoid additional churn as part of larger refactor.")] + internal override void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols) + { + // Check if the target method has a parameter of type MockBehavior + IParameterSymbol? mockParameter = target.Parameters.DefaultIfNotSingle(parameter => parameter.Type.IsInstanceOf(knownSymbols.MockBehavior)); + + // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does + if (mockParameter is null && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken)) + { + if (!methodMatch.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) + { + return; + } + + ImmutableDictionary properties = new DiagnosticEditProperties + { + TypeOfEdit = DiagnosticEditProperties.EditType.Insert, + EditPosition = parameterMatch.Ordinal, + }.ToImmutableDictionary(); + + // Using a method that doesn't accept a MockBehavior parameter, however there's an overload that does + context.ReportDiagnostic(context.Operation.CreateDiagnostic(Rule, properties)); + return; + } + + IArgumentOperation? mockArgument = arguments.DefaultIfNotSingle(argument => argument.Parameter.IsInstanceOf(mockParameter)); + + // Is the behavior set via a default value? + if (mockArgument?.ArgumentKind == ArgumentKind.DefaultValue && mockArgument.Value.WalkDownConversion().ConstantValue.Value == knownSymbols.MockBehaviorDefault?.ConstantValue) + { + if (!target.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) + { + return; + } + + ImmutableDictionary properties = new DiagnosticEditProperties + { + TypeOfEdit = DiagnosticEditProperties.EditType.Insert, + EditPosition = parameterMatch.Ordinal, + }.ToImmutableDictionary(); + + context.ReportDiagnostic(context.Operation.CreateDiagnostic(Rule, properties)); + return; + } + + // NOTE: This logic can't handle indirection (e.g. var x = MockBehavior.Default; new Mock(x);) + // + // The operation specifies a MockBehavior; is it MockBehavior.Strict? + if (mockArgument?.Value.WalkDownConversion().ConstantValue.Value != knownSymbols.MockBehaviorStrict?.ConstantValue + && mockArgument?.DescendantsAndSelf().OfType().Any(argument => argument.Member.IsInstanceOf(knownSymbols.MockBehaviorStrict)) != true) + { + if (!target.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) + { + return; + } + + ImmutableDictionary properties = new DiagnosticEditProperties + { + TypeOfEdit = DiagnosticEditProperties.EditType.Replace, + EditPosition = parameterMatch.Ordinal, + }.ToImmutableDictionary(); + + context.ReportDiagnostic(context.Operation.CreateDiagnostic(Rule, properties)); + } + } +} diff --git a/src/CodeFixes/BehaviorType.cs b/src/CodeFixes/BehaviorType.cs new file mode 100644 index 0000000..ef2d641 --- /dev/null +++ b/src/CodeFixes/BehaviorType.cs @@ -0,0 +1,23 @@ +namespace Moq.CodeFixes; + +/// +/// Options to customize the behavior of Moq. +/// +/// +/// Local copy of Moq's MockBehavior enum to avoid dependency on Moq library. +/// +internal enum BehaviorType +{ + /// + /// Will never throw exceptions, returning default values when necessary + /// ( for reference types, zero for value types, + /// or empty for enumerables and arrays). + /// + Loose, + + /// + /// Causes Moq to always throw an exception for invocations that don't have + /// a corresponding Setup. + /// + Strict, +} diff --git a/src/CodeFixes/SetExplicitMockBehaviorCodeAction.cs b/src/CodeFixes/SetExplicitMockBehaviorCodeAction.cs new file mode 100644 index 0000000..d2455e0 --- /dev/null +++ b/src/CodeFixes/SetExplicitMockBehaviorCodeAction.cs @@ -0,0 +1,65 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Simplification; + +namespace Moq.CodeFixes; + +internal sealed class SetExplicitMockBehaviorCodeAction : CodeAction +{ + private readonly Document _document; + private readonly SyntaxNode _nodeToFix; + private readonly BehaviorType _behaviorType; + private readonly DiagnosticEditProperties.EditType _editType; + private readonly int _position; + + public SetExplicitMockBehaviorCodeAction(string title, Document document, SyntaxNode nodeToFix, BehaviorType behaviorType, DiagnosticEditProperties.EditType editType, int position) + { + Title = title; + _document = document; + _nodeToFix = nodeToFix; + _behaviorType = behaviorType; + _editType = editType; + _position = position; + } + + public override string Title { get; } + + public override string? EquivalenceKey => Title; + + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + { + DocumentEditor editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); + SemanticModel? model = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + IOperation? operation = model?.GetOperation(_nodeToFix, cancellationToken); + + MoqKnownSymbols knownSymbols = new(editor.SemanticModel.Compilation); + + if (knownSymbols.MockBehavior is null + || knownSymbols.MockBehaviorDefault is null + || knownSymbols.MockBehaviorLoose is null + || knownSymbols.MockBehaviorStrict is null + || operation is null) + { + return _document; + } + + SyntaxNode behavior = _behaviorType switch + { + BehaviorType.Loose => editor.Generator.MemberAccessExpression(knownSymbols.MockBehaviorLoose), + BehaviorType.Strict => editor.Generator.MemberAccessExpression(knownSymbols.MockBehaviorStrict), + _ => throw new InvalidOperationException(), + }; + + SyntaxNode argument = editor.Generator.Argument(behavior); + + SyntaxNode newNode = _editType switch + { + DiagnosticEditProperties.EditType.Insert => editor.Generator.InsertArguments(operation, _position, argument), + DiagnosticEditProperties.EditType.Replace => editor.Generator.ReplaceArgument(operation, _position, argument), + _ => throw new InvalidOperationException(), + }; + + editor.ReplaceNode(_nodeToFix, newNode.WithAdditionalAnnotations(Simplifier.Annotation)); + return editor.GetChangedDocument(); + } +} diff --git a/src/CodeFixes/SetExplicitMockBehaviorFixer.cs b/src/CodeFixes/SetExplicitMockBehaviorFixer.cs index a6aa162..3952a0e 100644 --- a/src/CodeFixes/SetExplicitMockBehaviorFixer.cs +++ b/src/CodeFixes/SetExplicitMockBehaviorFixer.cs @@ -13,12 +13,6 @@ namespace Moq.CodeFixes; [Shared] public class SetExplicitMockBehaviorFixer : CodeFixProvider { - private enum BehaviorType - { - Loose, - Strict, - } - /// public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIds.SetExplicitMockBehavior); @@ -44,64 +38,4 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix(new SetExplicitMockBehaviorCodeAction("Set MockBehavior (Loose)", context.Document, nodeToFix, BehaviorType.Loose, editProperties.TypeOfEdit, editProperties.EditPosition), context.Diagnostics); context.RegisterCodeFix(new SetExplicitMockBehaviorCodeAction("Set MockBehavior (Strict)", context.Document, nodeToFix, BehaviorType.Strict, editProperties.TypeOfEdit, editProperties.EditPosition), context.Diagnostics); } - - private sealed class SetExplicitMockBehaviorCodeAction : CodeAction - { - private readonly Document _document; - private readonly SyntaxNode _nodeToFix; - private readonly BehaviorType _behaviorType; - private readonly DiagnosticEditProperties.EditType _editType; - private readonly int _position; - - public SetExplicitMockBehaviorCodeAction(string title, Document document, SyntaxNode nodeToFix, BehaviorType behaviorType, DiagnosticEditProperties.EditType editType, int position) - { - Title = title; - _document = document; - _nodeToFix = nodeToFix; - _behaviorType = behaviorType; - _editType = editType; - _position = position; - } - - public override string Title { get; } - - public override string? EquivalenceKey => Title; - - protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) - { - DocumentEditor editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); - SemanticModel? model = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - IOperation? operation = model?.GetOperation(_nodeToFix, cancellationToken); - - MoqKnownSymbols knownSymbols = new(editor.SemanticModel.Compilation); - - if (knownSymbols.MockBehavior is null - || knownSymbols.MockBehaviorDefault is null - || knownSymbols.MockBehaviorLoose is null - || knownSymbols.MockBehaviorStrict is null - || operation is null) - { - return _document; - } - - SyntaxNode behavior = _behaviorType switch - { - BehaviorType.Loose => editor.Generator.MemberAccessExpression(knownSymbols.MockBehaviorLoose), - BehaviorType.Strict => editor.Generator.MemberAccessExpression(knownSymbols.MockBehaviorStrict), - _ => throw new InvalidOperationException(), - }; - - SyntaxNode argument = editor.Generator.Argument(behavior); - - SyntaxNode newNode = _editType switch - { - DiagnosticEditProperties.EditType.Insert => editor.Generator.InsertArguments(operation, _position, argument), - DiagnosticEditProperties.EditType.Replace => editor.Generator.ReplaceArgument(operation, _position, argument), - _ => throw new InvalidOperationException(), - }; - - editor.ReplaceNode(_nodeToFix, newNode.WithAdditionalAnnotations(Simplifier.Annotation)); - return editor.GetChangedDocument(); - } - } } diff --git a/src/CodeFixes/SetStrictMockBehaviorFixer.cs b/src/CodeFixes/SetStrictMockBehaviorFixer.cs new file mode 100644 index 0000000..af0f5dd --- /dev/null +++ b/src/CodeFixes/SetStrictMockBehaviorFixer.cs @@ -0,0 +1,37 @@ +using System.Composition; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Moq.CodeFixes; + +/// +/// Fixes for . +/// +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SetStrictMockBehaviorFixer))] +[Shared] +public class SetStrictMockBehaviorFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticIds.SetStrictMockBehavior); + + /// + public override FixAllProvider? GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + SyntaxNode? nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + + if (!context.TryGetEditProperties(out DiagnosticEditProperties? editProperties)) + { + return; + } + + if (nodeToFix is null) + { + return; + } + + context.RegisterCodeFix(new SetExplicitMockBehaviorCodeAction("Set MockBehavior (Strict)", context.Document, nodeToFix, BehaviorType.Strict, editProperties.TypeOfEdit, editProperties.EditPosition), context.Diagnostics); + } +} diff --git a/src/Common/DiagnosticIds.cs b/src/Common/DiagnosticIds.cs index bb9e853..6a2d512 100644 --- a/src/Common/DiagnosticIds.cs +++ b/src/Common/DiagnosticIds.cs @@ -13,4 +13,5 @@ internal static class DiagnosticIds internal const string AsyncUsesReturnsAsyncInsteadOfResult = "Moq1201"; internal const string AsShouldOnlyBeUsedForInterfacesRuleId = "Moq1300"; internal const string SetExplicitMockBehavior = "Moq1400"; + internal const string SetStrictMockBehavior = "Moq1410"; } diff --git a/src/Common/WellKnown/MoqKnownSymbols.cs b/src/Common/WellKnown/MoqKnownSymbols.cs index d245236..7314e24 100644 --- a/src/Common/WellKnown/MoqKnownSymbols.cs +++ b/src/Common/WellKnown/MoqKnownSymbols.cs @@ -5,12 +5,12 @@ namespace Moq.Analyzers.Common.WellKnown; internal class MoqKnownSymbols : KnownSymbols { - public MoqKnownSymbols(WellKnownTypeProvider typeProvider) + internal MoqKnownSymbols(WellKnownTypeProvider typeProvider) : base(typeProvider) { } - public MoqKnownSymbols(Compilation compilation) + internal MoqKnownSymbols(Compilation compilation) : base(compilation) { } @@ -18,37 +18,37 @@ public MoqKnownSymbols(Compilation compilation) /// /// Gets the class Moq.Mock. /// - public INamedTypeSymbol? Mock => TypeProvider.GetOrCreateTypeByMetadataName("Moq.Mock"); + internal INamedTypeSymbol? Mock => TypeProvider.GetOrCreateTypeByMetadataName("Moq.Mock"); /// /// Gets the methods for Moq.Mock.As. /// - public ImmutableArray MockAs => Mock?.GetMembers("As").OfType().ToImmutableArray() ?? ImmutableArray.Empty; + internal ImmutableArray MockAs => Mock?.GetMembers("As").OfType().ToImmutableArray() ?? ImmutableArray.Empty; /// /// Gets the methods for Moq.Mock.Of. /// - public ImmutableArray MockOf => Mock?.GetMembers("Of").OfType().ToImmutableArray() ?? ImmutableArray.Empty; + internal ImmutableArray MockOf => Mock?.GetMembers("Of").OfType().ToImmutableArray() ?? ImmutableArray.Empty; /// /// Gets the class Moq.Mock<T>. /// - public INamedTypeSymbol? Mock1 => TypeProvider.GetOrCreateTypeByMetadataName("Moq.Mock`1"); + internal INamedTypeSymbol? Mock1 => TypeProvider.GetOrCreateTypeByMetadataName("Moq.Mock`1"); /// /// Gets the methods for Moq.Mock<T>.As. /// - public ImmutableArray Mock1As => Mock1?.GetMembers("As").OfType().ToImmutableArray() ?? ImmutableArray.Empty; + internal ImmutableArray Mock1As => Mock1?.GetMembers("As").OfType().ToImmutableArray() ?? ImmutableArray.Empty; /// /// Gets the methods for Moq.Mock<T>.Setup. /// - public ImmutableArray Mock1Setup => Mock1?.GetMembers("Setup").OfType().ToImmutableArray() ?? ImmutableArray.Empty; + internal ImmutableArray Mock1Setup => Mock1?.GetMembers("Setup").OfType().ToImmutableArray() ?? ImmutableArray.Empty; /// /// Gets the class Moq.MockRepository. /// - public INamedTypeSymbol? MockRepository => TypeProvider.GetOrCreateTypeByMetadataName("Moq.MockRepository"); + internal INamedTypeSymbol? MockRepository => TypeProvider.GetOrCreateTypeByMetadataName("Moq.MockRepository"); /// /// Gets the methods for Moq.MockRepository.Of. @@ -60,25 +60,25 @@ public MoqKnownSymbols(Compilation compilation) /// when looking for members. /// [SuppressMessage("Performance", "ECS0900:Minimize boxing and unboxing", Justification = "Minor perf issues. Should revisit later.")] - public ImmutableArray MockRepositoryCreate => MockRepository?.GetBaseTypesAndThis().SelectMany(type => type.GetMembers("Create")).OfType().ToImmutableArray() ?? ImmutableArray.Empty; + internal ImmutableArray MockRepositoryCreate => MockRepository?.GetBaseTypesAndThis().SelectMany(type => type.GetMembers("Create")).OfType().ToImmutableArray() ?? ImmutableArray.Empty; /// /// Gets the enum Moq.MockBehavior. /// - public INamedTypeSymbol? MockBehavior => TypeProvider.GetOrCreateTypeByMetadataName("Moq.MockBehavior"); + internal INamedTypeSymbol? MockBehavior => TypeProvider.GetOrCreateTypeByMetadataName("Moq.MockBehavior"); /// /// Gets the field Moq.MockBehavior.Strict. /// - public IFieldSymbol? MockBehaviorStrict => MockBehavior?.GetMembers("Strict").OfType().SingleOrDefault(); + internal IFieldSymbol? MockBehaviorStrict => MockBehavior?.GetMembers("Strict").OfType().SingleOrDefault(); /// /// Gets the field Moq.MockBehavior.Loose. /// - public IFieldSymbol? MockBehaviorLoose => MockBehavior?.GetMembers("Loose").OfType().SingleOrDefault(); + internal IFieldSymbol? MockBehaviorLoose => MockBehavior?.GetMembers("Loose").OfType().SingleOrDefault(); /// /// Gets the field Moq.MockBehavior.Default. /// - public IFieldSymbol? MockBehaviorDefault => MockBehavior?.GetMembers("Default").OfType().SingleOrDefault(); + internal IFieldSymbol? MockBehaviorDefault => MockBehavior?.GetMembers("Default").OfType().SingleOrDefault(); } diff --git a/tests/Moq.Analyzers.Test/SetStrictMockBehaviorCodeFixTests.cs b/tests/Moq.Analyzers.Test/SetStrictMockBehaviorCodeFixTests.cs new file mode 100644 index 0000000..5ca675e --- /dev/null +++ b/tests/Moq.Analyzers.Test/SetStrictMockBehaviorCodeFixTests.cs @@ -0,0 +1,136 @@ +using Verifier = Moq.Analyzers.Test.Helpers.CodeFixVerifier; + +namespace Moq.Analyzers.Test; + +public class SetStrictMockBehaviorCodeFixTests +{ + private readonly ITestOutputHelper _output; + + public SetStrictMockBehaviorCodeFixTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable TestData() + { + IEnumerable mockConstructors = new object[][] + { + [ + """{|Moq1410:new Mock()|};""", + """new Mock(MockBehavior.Strict);""", + ], + [ + """{|Moq1410:new Mock(MockBehavior.Default)|};""", + """new Mock(MockBehavior.Strict);""", + ], + [ + """{|Moq1410:new Mock(MockBehavior.Loose)|};""", + """new Mock(MockBehavior.Strict);""", + ], + [ + """new Mock(MockBehavior.Strict);""", + """new Mock(MockBehavior.Strict);""", + ], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + + IEnumerable mockConstructorsWithExpressions = new object[][] + { + [ + """{|Moq1410:new Mock(() => new Calculator())|};""", + """new Mock(() => new Calculator(), MockBehavior.Strict);""", + ], + [ + """{|Moq1410:new Mock(() => new Calculator(), MockBehavior.Default)|};""", + """new Mock(() => new Calculator(), MockBehavior.Strict);""", + ], + [ + """{|Moq1410:new Mock(() => new Calculator(), MockBehavior.Loose)|};""", + """new Mock(() => new Calculator(), MockBehavior.Strict);""", + ], + [ + """new Mock(() => new Calculator(), MockBehavior.Strict);""", + """new Mock(() => new Calculator(), MockBehavior.Strict);""", + ], + }.WithNamespaces().WithNewMoqReferenceAssemblyGroups(); + + IEnumerable fluentBuilders = new object[][] + { + [ + """{|Moq1410:Mock.Of()|};""", + """Mock.Of(MockBehavior.Strict);""", + ], + [ + """{|Moq1410:Mock.Of(MockBehavior.Default)|};""", + """Mock.Of(MockBehavior.Strict);""", + ], + [ + """{|Moq1410:Mock.Of(MockBehavior.Loose)|};""", + """Mock.Of(MockBehavior.Strict);""", + ], + [ + """Mock.Of(MockBehavior.Strict);""", + """Mock.Of(MockBehavior.Strict);""", + ], + }.WithNamespaces().WithNewMoqReferenceAssemblyGroups(); + + IEnumerable mockRepositories = new object[][] + { + [ + """{|Moq1410:new MockRepository(MockBehavior.Default)|};""", + """new MockRepository(MockBehavior.Strict);""", + ], + [ + """{|Moq1410:new MockRepository(MockBehavior.Loose)|};""", + """new MockRepository(MockBehavior.Strict);""", + ], + [ + """new MockRepository(MockBehavior.Strict);""", + """new MockRepository(MockBehavior.Strict);""", + ], + }.WithNamespaces().WithNewMoqReferenceAssemblyGroups(); + + return mockConstructors.Union(mockConstructorsWithExpressions).Union(fluentBuilders).Union(mockRepositories); + } + + [Theory] + [MemberData(nameof(TestData))] + public async Task ShouldAnalyzeMocksWithoutExplicitMockBehavior(string referenceAssemblyGroup, string @namespace, string original, string quickFix) + { + static string Template(string ns, string mock) => + $$""" + {{ns}} + + public interface ISample + { + int Calculate(int a, int b); + } + + public class Calculator + { + public int Calculate(int a, int b) + { + return a + b; + } + } + + internal class UnitTest + { + private void Test() + { + {{mock}} + } + } + """; + + string o = Template(@namespace, original); + string f = Template(@namespace, quickFix); + + _output.WriteLine("Original:"); + _output.WriteLine(o); + _output.WriteLine(string.Empty); + _output.WriteLine("Fixed:"); + _output.WriteLine(f); + + await Verifier.VerifyCodeFixAsync(o, f, referenceAssemblyGroup); + } +}