Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .markdownlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"MD013": false,
"MD024": false,
"MD033": false,
"MD041": false
"MD041": false,
"MD060": false
}
60 changes: 60 additions & 0 deletions docs/rules/Moq1003.md
Original file line number Diff line number Diff line change
@@ -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<T>`, 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<MyService>(); // 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<MyService>(); // 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<MyService>(); // 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).
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
4 changes: 3 additions & 1 deletion src/Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
; <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)
Expand Down
205 changes: 205 additions & 0 deletions src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using Microsoft.CodeAnalysis.Operations;

Check warning on line 1 in src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs#L1

Provide an 'AssemblyVersion' attribute for assembly 'srcassembly.dll'.
using Moq.Analyzers.Common;

namespace Moq.Analyzers;

/// <summary>
/// Detects when <c>Mock&lt;T&gt;</c> is used where <c>T</c> is an <see langword="internal"/> type
/// and the assembly containing <c>T</c> does not have
/// <c>[InternalsVisibleTo("DynamicProxyGenAssembly2")]</c>.
/// </summary>
[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");

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

/// <inheritdoc />
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(

Check warning on line 62 in src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Analyzers/InternalTypeMustHaveInternalsVisibleToAnalyzer.cs#L62

Method InternalTypeMustHaveInternalsVisibleToAnalyzer::Analyze has a cyclomatic complexity of 10 (limit is 8)
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)));
}
}

/// <summary>
/// Determines whether the mocked type is effectively internal and its assembly
/// lacks InternalsVisibleTo for DynamicProxy.
/// </summary>
private static bool ShouldReportDiagnostic(
ITypeSymbol mockedType,
INamedTypeSymbol? internalsVisibleToAttribute)
{
if (!IsEffectivelyInternal(mockedType))
{
return false;
}

return !HasInternalsVisibleToDynamicProxy(mockedType.ContainingAssembly, internalsVisibleToAttribute);
}

/// <summary>
/// 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:
/// <list type="bullet">
/// <item><see cref="Accessibility.Internal"/> (internal)</item>
/// <item><see cref="Accessibility.ProtectedOrInternal"/> (protected internal) on
/// a containing type, because DynamicProxy does not derive from the container</item>
/// </list>
/// Note: <see cref="Accessibility.Private"/>, <see cref="Accessibility.Protected"/>,
/// and <see cref="Accessibility.ProtectedAndInternal"/> (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.
/// </summary>
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;
}
Comment thread
cursor[bot] marked this conversation as resolved.

/// <summary>
/// Checks the assembly's attributes for InternalsVisibleTo targeting DynamicProxy,
/// using symbol-based comparison for the attribute type.
/// </summary>
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;
}

/// <summary>
/// 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.
/// </summary>
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] == ',';
}
}
28 changes: 1 addition & 27 deletions src/Analyzers/NoMockOfLoggerAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -100,32 +100,6 @@ private static void Analyze(OperationAnalysisContext context, MoqKnownSymbols kn
}
}

/// <summary>
/// Determines if the operation is a valid Mock.Of{T}() or MockRepository.Create{T}() invocation
/// and extracts the mocked type.
/// </summary>
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;
}

/// <summary>
/// Determines whether the mocked type is ILogger or ILogger{T} using symbol-based comparison.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Common/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
31 changes: 31 additions & 0 deletions src/Common/MockDetectionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,37 @@ public static bool IsValidMockOfInvocation(IInvocationOperation invocation, MoqK
return false;
}

/// <summary>
/// Determines if the operation is a valid Mock.Of{T}() or MockRepository.Create{T}() invocation
/// and extracts the mocked type.
/// </summary>
/// <param name="invocation">The invocation operation.</param>
/// <param name="knownSymbols">The known Moq symbols.</param>
/// <param name="mockedType">When successful, the mocked type; otherwise, null.</param>
/// <returns>True if this is a valid mock invocation; otherwise, false.</returns>
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;
}

/// <summary>
/// Checks if the method symbol represents a static Mock.Of{T}() method.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Common/WellKnown/KnownSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,10 @@ public KnownSymbols(Compilation compilation)
/// </summary>
public INamedTypeSymbol? Action1 => TypeProvider.GetOrCreateTypeByMetadataName("System.Action`1");

/// <summary>
/// Gets the class <see cref="System.Runtime.CompilerServices.InternalsVisibleToAttribute"/>.
/// </summary>
public INamedTypeSymbol? InternalsVisibleToAttribute => TypeProvider.GetOrCreateTypeByMetadataName("System.Runtime.CompilerServices.InternalsVisibleToAttribute");

protected WellKnownTypeProvider TypeProvider { get; }
}
Loading
Loading