diff --git a/.markdownlint.json b/.markdownlint.json index 484d3db4c..df6139201 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -2,5 +2,6 @@ "MD013": false, "MD024": false, "MD033": false, - "MD041": false + "MD041": false, + "MD060": false } diff --git a/docs/rules/Moq1003.md b/docs/rules/Moq1003.md new file mode 100644 index 000000000..f827c64b7 --- /dev/null +++ b/docs/rules/Moq1003.md @@ -0,0 +1,60 @@ +# Moq1003: Internal type requires InternalsVisibleTo for DynamicProxy + +| Item | Value | +| -------- | ------- | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | + +--- + +When mocking an `internal` type with `Mock`, Castle DynamicProxy needs access to the type's internals to generate a proxy at runtime. Without `[InternalsVisibleTo("DynamicProxyGenAssembly2")]` on the assembly containing the internal type, the mock will fail at runtime. + +To fix: + +- Add `[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]` to the assembly containing the internal type +- Make the type `public` if appropriate +- Introduce a public interface and mock that instead + +## Examples of patterns that are flagged by this analyzer + +```csharp +// Assembly without InternalsVisibleTo +internal class MyService { } + +var mock = new Mock(); // Moq1003: Internal type requires InternalsVisibleTo +``` + +## Solution + +```csharp +// Add to AssemblyInfo.cs or any file in the project containing the internal type +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DynamicProxyGenAssembly2")] + +internal class MyService { } + +var mock = new Mock(); // OK +``` + +## 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 Moq1003 +var mock = new Mock(); // Moq1003: Internal type requires InternalsVisibleTo +#pragma warning restore Moq1003 +``` + +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.Moq1003.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 ae05b48bb..ea8c0287b 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -5,6 +5,7 @@ | [Moq1000](./Moq1000.md) | Usage | Sealed classes cannot be mocked | [NoSealedClassMocksAnalyzer.cs](../../src/Analyzers/NoSealedClassMocksAnalyzer.cs) | | [Moq1001](./Moq1001.md) | Usage | Mocked interfaces cannot have constructor parameters | [ConstructorArgumentsShouldMatchAnalyzer.cs](../../src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs) | | [Moq1002](./Moq1002.md) | Usage | Parameters provided into mock do not match any existing constructors | [ConstructorArgumentsShouldMatchAnalyzer.cs](../../src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs) | +| [Moq1003](./Moq1003.md) | Usage | Internal type requires InternalsVisibleTo for DynamicProxy | [InternalTypeMustHaveInternalsVisibleToAnalyzer.cs](../../src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs) | | [Moq1004](./Moq1004.md) | Usage | ILogger should not be mocked | [NoMockOfLoggerAnalyzer.cs](../../src/Analyzers/NoMockOfLoggerAnalyzer.cs) | | [Moq1100](./Moq1100.md) | Correctness | Callback signature must match the signature of the mocked method | [CallbackSignatureShouldMatchMockedMethodAnalyzer.cs](../../src/Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs) | | [Moq1101](./Moq1101.md) | Usage | SetupGet/SetupSet/SetupProperty should be used for properties, not for methods | [NoMethodsInPropertySetupAnalyzer.cs](../../src/Analyzers/NoMethodsInPropertySetupAnalyzer.cs) | diff --git a/src/Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/AnalyzerReleases.Unshipped.md index 74bbd0562..3589f1c95 100644 --- a/src/Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/AnalyzerReleases.Unshipped.md @@ -1,12 +1,14 @@ ; Unshipped analyzer release -; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +; ### New Rules + Rule ID | Category | Severity | Notes --------|----------|----------|------- Moq1000 | Usage | Warning | NoSealedClassMocksAnalyzer (updated category from Moq to Usage) Moq1001 | Usage | Warning | NoConstructorArgumentsForInterfaceMockRuleId (updated category from Moq to Usage) Moq1002 | Usage | Warning | NoMatchingConstructorRuleId (updated category from Moq to Usage) +Moq1003 | Usage | Warning | InternalTypeMustHaveInternalsVisibleToAnalyzer Moq1004 | Usage | Warning | NoMockOfLoggerAnalyzer Moq1100 | Usage | Warning | CallbackSignatureShouldMatchMockedMethodAnalyzer (updated category from Moq to Usage) Moq1101 | Usage | Warning | NoMethodsInPropertySetupAnalyzer (updated category from Moq to Usage) diff --git a/src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs b/src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs new file mode 100644 index 000000000..b96cf3eb4 --- /dev/null +++ b/src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs @@ -0,0 +1,205 @@ +using Microsoft.CodeAnalysis.Operations; +using Moq.Analyzers.Common; + +namespace Moq.Analyzers; + +/// +/// Detects when Mock<T> is used where T is an type +/// and the assembly containing T does not have +/// [InternalsVisibleTo("DynamicProxyGenAssembly2")]. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class InternalTypeMustHaveInternalsVisibleToAnalyzer : DiagnosticAnalyzer +{ + private static readonly string DynamicProxyAssemblyName = "DynamicProxyGenAssembly2"; + + private static readonly LocalizableString Title = "Moq: Internal type requires InternalsVisibleTo"; + private static readonly LocalizableString Message = "Internal type '{0}' requires [InternalsVisibleTo(\"DynamicProxyGenAssembly2\")] in its assembly to be mocked"; + private static readonly LocalizableString Description = "Mocking internal types requires the assembly to grant access to Castle DynamicProxy via InternalsVisibleTo."; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticIds.InternalTypeMustHaveInternalsVisibleTo, + Title, + Message, + DiagnosticCategory.Usage, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: Description, + helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/{ThisAssembly.GitCommitId}/docs/rules/{DiagnosticIds.InternalTypeMustHaveInternalsVisibleTo}.md"); + + /// + 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); + + if (!knownSymbols.IsMockReferenced()) + { + return; + } + + if (knownSymbols.Mock1 is null) + { + return; + } + + context.RegisterOperationAction( + operationAnalysisContext => Analyze(operationAnalysisContext, knownSymbols), + OperationKind.ObjectCreation, + OperationKind.Invocation); + } + + private static void Analyze( + OperationAnalysisContext context, + MoqKnownSymbols knownSymbols) + { + ITypeSymbol? mockedType = null; + Location? diagnosticLocation = null; + + if (context.Operation is IObjectCreationOperation creation && + MockDetectionHelpers.IsValidMockCreation(creation, knownSymbols, out mockedType)) + { + diagnosticLocation = MockDetectionHelpers.GetDiagnosticLocation(context.Operation, creation.Syntax); + } + else if (context.Operation is IInvocationOperation invocation && + MockDetectionHelpers.IsValidMockInvocation(invocation, knownSymbols, out mockedType)) + { + diagnosticLocation = MockDetectionHelpers.GetDiagnosticLocation(context.Operation, invocation.Syntax); + } + else + { + return; + } + + if (mockedType != null && diagnosticLocation != null && + ShouldReportDiagnostic(mockedType, knownSymbols.InternalsVisibleToAttribute)) + { + context.ReportDiagnostic(diagnosticLocation.CreateDiagnostic( + Rule, + mockedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat))); + } + } + + /// + /// Determines whether the mocked type is effectively internal and its assembly + /// lacks InternalsVisibleTo for DynamicProxy. + /// + private static bool ShouldReportDiagnostic( + ITypeSymbol mockedType, + INamedTypeSymbol? internalsVisibleToAttribute) + { + if (!IsEffectivelyInternal(mockedType)) + { + return false; + } + + return !HasInternalsVisibleToDynamicProxy(mockedType.ContainingAssembly, internalsVisibleToAttribute); + } + + /// + /// Checks if the type (or any containing type) has accessibility that requires + /// InternalsVisibleTo for DynamicProxy to access it. DynamicProxy resides in a + /// separate assembly and does not derive from containing types, so it relies on + /// assembly-level access. Any of the following accessibility levels on the type + /// or its containers make it inaccessible to DynamicProxy without InternalsVisibleTo: + /// + /// (internal) + /// (protected internal) on + /// a containing type, because DynamicProxy does not derive from the container + /// + /// Note: , , + /// and (private protected) are excluded + /// because InternalsVisibleTo cannot help with those - private types are only accessible + /// within their declaring type, and protected/private protected types require inheritance + /// from the containing type, which DynamicProxy does not provide. + /// + private static bool IsEffectivelyInternal(ITypeSymbol type) + { + ITypeSymbol? current = type; + while (current != null) + { + switch (current.DeclaredAccessibility) + { + case Accessibility.Internal: + case Accessibility.ProtectedOrInternal: + return true; + } + + current = current.ContainingType; + } + + return false; + } + + /// + /// Checks the assembly's attributes for InternalsVisibleTo targeting DynamicProxy, + /// using symbol-based comparison for the attribute type. + /// + private static bool HasInternalsVisibleToDynamicProxy( + IAssemblySymbol? assembly, + INamedTypeSymbol? internalsVisibleToAttribute) + { + if (assembly is null) + { + return false; + } + + // If we cannot resolve InternalsVisibleToAttribute (highly unlikely), bail out + // conservatively by not reporting a diagnostic (avoiding false positives). + if (internalsVisibleToAttribute is null) + { + return true; + } + + foreach (AttributeData attribute in assembly.GetAttributes()) + { + if (attribute.AttributeClass is null) + { + continue; + } + + // Symbol-based comparison instead of string-based ToDisplayString() + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, internalsVisibleToAttribute)) + { + continue; + } + + if (attribute.ConstructorArguments.Length == 1 && + attribute.ConstructorArguments[0].Value is string assemblyName && + IsDynamicProxyAssemblyName(assemblyName)) + { + return true; + } + } + + return false; + } + + /// + /// Checks if the assembly name matches DynamicProxy. The InternalsVisibleTo attribute + /// value can be either the simple name ("DynamicProxyGenAssembly2") or include a + /// public key token ("DynamicProxyGenAssembly2, PublicKey=..."). We match the exact + /// name followed by either end-of-string or a comma separator. + /// + private static bool IsDynamicProxyAssemblyName(string assemblyName) + { + if (!assemblyName.StartsWith(DynamicProxyAssemblyName, StringComparison.Ordinal)) + { + return false; + } + + // Must be exact match or followed by comma (for public key suffix) + return assemblyName.Length == DynamicProxyAssemblyName.Length || + assemblyName[DynamicProxyAssemblyName.Length] == ','; + } +} diff --git a/src/Analyzers/NoMockOfLoggerAnalyzer.cs b/src/Analyzers/NoMockOfLoggerAnalyzer.cs index 0b79715bf..f946a1ec2 100644 --- a/src/Analyzers/NoMockOfLoggerAnalyzer.cs +++ b/src/Analyzers/NoMockOfLoggerAnalyzer.cs @@ -84,7 +84,7 @@ private static void Analyze(OperationAnalysisContext context, MoqKnownSymbols kn // Handle static method invocation: Mock.Of{T}() or MockRepository.Create{T}() else if (context.Operation is IInvocationOperation invocation && - IsValidMockInvocation(invocation, knownSymbols, out mockedType)) + MockDetectionHelpers.IsValidMockInvocation(invocation, knownSymbols, out mockedType)) { diagnosticLocation = MockDetectionHelpers.GetDiagnosticLocation(context.Operation, invocation.Syntax); } @@ -100,32 +100,6 @@ private static void Analyze(OperationAnalysisContext context, MoqKnownSymbols kn } } - /// - /// Determines if the operation is a valid Mock.Of{T}() or MockRepository.Create{T}() invocation - /// and extracts the mocked type. - /// - private static bool IsValidMockInvocation(IInvocationOperation invocation, MoqKnownSymbols knownSymbols, [NotNullWhen(true)] out ITypeSymbol? mockedType) - { - mockedType = null; - - bool isMockOf = MockDetectionHelpers.IsValidMockOfMethod(invocation.TargetMethod, knownSymbols); - bool isMockRepositoryCreate = !isMockOf && invocation.TargetMethod.IsInstanceOf(knownSymbols.MockRepositoryCreate); - - if (!isMockOf && !isMockRepositoryCreate) - { - return false; - } - - // Both Mock.Of{T}() and MockRepository.Create{T}() use a single type argument - if (invocation.TargetMethod.TypeArguments.Length == 1) - { - mockedType = invocation.TargetMethod.TypeArguments[0]; - return true; - } - - return false; - } - /// /// Determines whether the mocked type is ILogger or ILogger{T} using symbol-based comparison. /// diff --git a/src/Common/DiagnosticIds.cs b/src/Common/DiagnosticIds.cs index f11c72b88..92691437d 100644 --- a/src/Common/DiagnosticIds.cs +++ b/src/Common/DiagnosticIds.cs @@ -7,6 +7,7 @@ internal static class DiagnosticIds internal const string SealedClassCannotBeMocked = "Moq1000"; internal const string NoConstructorArgumentsForInterfaceMockRuleId = "Moq1001"; internal const string NoMatchingConstructorRuleId = "Moq1002"; + internal const string InternalTypeMustHaveInternalsVisibleTo = "Moq1003"; internal const string LoggerShouldNotBeMocked = "Moq1004"; internal const string BadCallbackParameters = "Moq1100"; internal const string PropertySetupUsedForMethod = "Moq1101"; diff --git a/src/Common/MockDetectionHelpers.cs b/src/Common/MockDetectionHelpers.cs index b9770aa63..32c0656ba 100644 --- a/src/Common/MockDetectionHelpers.cs +++ b/src/Common/MockDetectionHelpers.cs @@ -53,6 +53,37 @@ public static bool IsValidMockOfInvocation(IInvocationOperation invocation, MoqK return false; } + /// + /// Determines if the operation is a valid Mock.Of{T}() or MockRepository.Create{T}() invocation + /// and extracts the mocked type. + /// + /// The invocation operation. + /// The known Moq symbols. + /// When successful, the mocked type; otherwise, null. + /// True if this is a valid mock invocation; otherwise, false. + public static bool IsValidMockInvocation(IInvocationOperation invocation, MoqKnownSymbols knownSymbols, [NotNullWhen(true)] out ITypeSymbol? mockedType) + { + mockedType = null; + + IMethodSymbol targetMethod = invocation.TargetMethod; + + bool isMockOf = IsValidMockOfMethod(targetMethod, knownSymbols); + bool isMockRepositoryCreate = !isMockOf && targetMethod.IsInstanceOf(knownSymbols.MockRepositoryCreate); + + if (!isMockOf && !isMockRepositoryCreate) + { + return false; + } + + if (targetMethod.TypeArguments.Length == 1) + { + mockedType = targetMethod.TypeArguments[0]; + return true; + } + + return false; + } + /// /// Checks if the method symbol represents a static Mock.Of{T}() method. /// diff --git a/src/Common/WellKnown/KnownSymbols.cs b/src/Common/WellKnown/KnownSymbols.cs index 70e5dbb16..7007b3d35 100644 --- a/src/Common/WellKnown/KnownSymbols.cs +++ b/src/Common/WellKnown/KnownSymbols.cs @@ -57,5 +57,10 @@ public KnownSymbols(Compilation compilation) /// public INamedTypeSymbol? Action1 => TypeProvider.GetOrCreateTypeByMetadataName("System.Action`1"); + /// + /// Gets the class . + /// + public INamedTypeSymbol? InternalsVisibleToAttribute => TypeProvider.GetOrCreateTypeByMetadataName("System.Runtime.CompilerServices.InternalsVisibleToAttribute"); + protected WellKnownTypeProvider TypeProvider { get; } } diff --git a/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs b/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs index 3fa5e6c3e..9408fbb50 100644 --- a/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs +++ b/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing; namespace Moq.Analyzers.Test.Helpers; @@ -29,11 +29,17 @@ public static class ReferenceAssemblyCatalog /// public static string Net80WithNewMoqAndLogging => nameof(Net80WithNewMoqAndLogging); + /// + /// Gets the name of the reference assembly group for .NET 8.0 without Moq. + /// + public static string Net80 => nameof(Net80); + /// /// Gets the catalog of reference assemblies. /// /// - /// The key is the name of the reference assembly group ( and ). + /// The key is the name of the reference assembly group (, , + /// , and ). /// public static IReadOnlyDictionary Catalog { get; } = new Dictionary(StringComparer.Ordinal) { @@ -54,5 +60,8 @@ public static class ReferenceAssemblyCatalog new PackageIdentity("Microsoft.Extensions.Logging.Abstractions", "8.0.0"), ]) }, + + // .NET 8.0 without Moq, used to verify analyzers bail out gracefully when Moq is not referenced. + { nameof(Net80), ReferenceAssemblies.Net.Net80 }, }; } diff --git a/tests/Moq.Analyzers.Test/InternalTypeMustHaveInternalsVisibleToAnalyzerTests.cs b/tests/Moq.Analyzers.Test/InternalTypeMustHaveInternalsVisibleToAnalyzerTests.cs new file mode 100644 index 000000000..cd8701353 --- /dev/null +++ b/tests/Moq.Analyzers.Test/InternalTypeMustHaveInternalsVisibleToAnalyzerTests.cs @@ -0,0 +1,348 @@ +using Microsoft.CodeAnalysis.Testing; +using Verifier = Moq.Analyzers.Test.Helpers.AnalyzerVerifier; + +namespace Moq.Analyzers.Test; + +public class InternalTypeMustHaveInternalsVisibleToAnalyzerTests +{ + public static IEnumerable InternalTypeWithoutAttributeTestData() + { + return new object[][] + { + ["""new Mock<{|Moq1003:InternalClass|}>()"""], + ["""new Mock<{|Moq1003:InternalClass|}>(MockBehavior.Strict)"""], + ["""Mock.Of<{|Moq1003:InternalClass|}>()"""], + ["""var mock = new Mock<{|Moq1003:InternalClass|}>()"""], + ["""var repo = new MockRepository(MockBehavior.Strict); repo.Create<{|Moq1003:InternalClass|}>()"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } + + public static IEnumerable PublicTypeTestData() + { + return new object[][] + { + ["""new Mock()"""], + ["""new Mock(MockBehavior.Strict)"""], + ["""Mock.Of()"""], + ["""var mock = new Mock()"""], + ["""var repo = new MockRepository(MockBehavior.Strict); repo.Create()"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } + + public static IEnumerable InterfaceTestData() + { + return new object[][] + { + // Internal interfaces also need InternalsVisibleTo + ["""new Mock<{|Moq1003:IInternalInterface|}>()"""], + ["""Mock.Of<{|Moq1003:IInternalInterface|}>()"""], + + // Public interfaces should not trigger + ["""new Mock()"""], + ["""Mock.Of()"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } + + public static IEnumerable NestedTypeTestData() + { + return new object[][] + { + // Public type nested inside internal type is effectively internal + ["""new Mock<{|Moq1003:InternalOuter.PublicNested|}>()"""], + + // Internal type nested inside public type + ["""new Mock<{|Moq1003:PublicOuter.InternalNested|}>()"""], + + // Public nested in public should not trigger + ["""new Mock()"""], + }.WithNamespaces().WithMoqReferenceAssemblyGroups(); + } + + [Theory] + [MemberData(nameof(InternalTypeWithoutAttributeTestData))] + public async Task ShouldDetectInternalTypeWithoutInternalsVisibleTo(string referenceAssemblyGroup, string @namespace, string mock) + { + await Verifier.VerifyAnalyzerAsync( + $$""" + {{@namespace}} + + internal class InternalClass { public virtual void DoWork() { } } + + public class PublicClass { public virtual void DoWork() { } } + + internal class UnitTest + { + private void Test() + { + {{mock}}; + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(PublicTypeTestData))] + public async Task ShouldNotFlagPublicType(string referenceAssemblyGroup, string @namespace, string mock) + { + await Verifier.VerifyAnalyzerAsync( + $$""" + {{@namespace}} + + public class PublicClass { public virtual void DoWork() { } } + + internal class UnitTest + { + private void Test() + { + {{mock}}; + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(InterfaceTestData))] + public async Task ShouldHandleInterfaces(string referenceAssemblyGroup, string @namespace, string mock) + { + await Verifier.VerifyAnalyzerAsync( + $$""" + {{@namespace}} + + internal interface IInternalInterface { void DoWork(); } + + public interface IPublicInterface { void DoWork(); } + + internal class UnitTest + { + private void Test() + { + {{mock}}; + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(NestedTypeTestData))] + public async Task ShouldHandleNestedTypes(string referenceAssemblyGroup, string @namespace, string mock) + { + await Verifier.VerifyAnalyzerAsync( + $$""" + {{@namespace}} + + internal class InternalOuter + { + public class PublicNested { public virtual void DoWork() { } } + } + + public class PublicOuter + { + internal class InternalNested { public virtual void DoWork() { } } + public class PublicNested { public virtual void DoWork() { } } + } + + internal class UnitTest + { + private void Test() + { + {{mock}}; + } + } + """, + referenceAssemblyGroup); + } + + [Fact] + public async Task ShouldNotFlagInternalTypeWithCorrectInternalsVisibleTo() + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + using System.Runtime.CompilerServices; + + [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + + internal class InternalClass { public virtual void DoWork() { } } + + internal class UnitTest + { + private void Test() + { + var mock = new Mock(); + var of = Mock.Of(); + var repo = new MockRepository(MockBehavior.Strict); + repo.Create(); + } + } + """, + referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80WithOldMoq); + } + + [Fact] + public async Task ShouldFlagInternalTypeWithWrongAssemblyName() + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + using System.Runtime.CompilerServices; + + [assembly: InternalsVisibleTo("SomeOtherAssembly")] + + internal class InternalClass { public virtual void DoWork() { } } + + internal class UnitTest + { + private void Test() + { + var mock = new Mock<{|Moq1003:InternalClass|}>(); + var of = Mock.Of<{|Moq1003:InternalClass|}>(); + } + } + """, + referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80WithOldMoq); + } + + [Fact] + public async Task ShouldNotFlagInternalTypeWithPublicKeyInAttribute() + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + using System.Runtime.CompilerServices; + + [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] + + internal class InternalClass { public virtual void DoWork() { } } + + internal class UnitTest + { + private void Test() + { + var mock = new Mock(); + } + } + """, + referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80WithOldMoq); + } + + [Fact] + public async Task ShouldNotAnalyzeWhenMoqNotReferenced() + { + // Use Net80 (no Moq) so IsMockReferenced() returns false and the analyzer + // bails out early. CompilerDiagnostics.None suppresses errors caused by the + // global "using Moq;" that the test harness injects. + await Verifier.VerifyAnalyzerAsync( + """ + namespace Test + { + internal class InternalClass { } + + internal class UnitTest + { + private void Test() + { + var instance = new InternalClass(); + } + } + } + """, + referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80, + CompilerDiagnostics.None); + } + + [Fact] + public async Task ShouldNotFlagAbstractPublicType() + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public abstract class PublicAbstractClass { public abstract void DoWork(); } + + internal class UnitTest + { + private void Test() + { + var mock = new Mock(); + } + } + """, + referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80WithOldMoq); + } + + [Fact] + public async Task ShouldNotFlagWhenMultipleAttributesIncludeDynamicProxy() + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + using System.Runtime.CompilerServices; + + [assembly: InternalsVisibleTo("SomeOtherAssembly")] + [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + + internal class InternalClass { public virtual void DoWork() { } } + + internal class UnitTest + { + private void Test() + { + var mock = new Mock(); + } + } + """, + referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80WithOldMoq); + } + + [Fact] + public async Task ShouldFlagInternalTypeWithSimilarButWrongAssemblyName() + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + using System.Runtime.CompilerServices; + + [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2Extra")] + + internal class InternalClass { public virtual void DoWork() { } } + + internal class UnitTest + { + private void Test() + { + var mock = new Mock<{|Moq1003:InternalClass|}>(); + } + } + """, + referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80WithOldMoq); + } + + [Fact] + public async Task ShouldFlagProtectedInternalNestedType() + { + // protected internal nested type requires InternalsVisibleTo because + // DynamicProxy does not derive from the containing type and cannot + // access the type via the protected path. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public class PublicBase + { + protected internal class ProtectedInternalNested { public virtual void DoWork() { } } + } + + internal class UnitTest + { + private void Test() + { + var mock = new Mock<{|Moq1003:PublicBase.ProtectedInternalNested|}>(); + } + } + """, + referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80WithOldMoq); + } +}