-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add Moq1003 analyzer for internal types requiring InternalsVisibleTo #958
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
Changes from all commits
968ab07
fcb9ab5
d4522a3
85d73f0
28d6e40
0de6c9b
ba7194a
0507c74
49aaedd
da072d4
437c69d
7ce02ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,5 +2,6 @@ | |
| "MD013": false, | ||
| "MD024": false, | ||
| "MD033": false, | ||
| "MD041": false | ||
| "MD041": false, | ||
| "MD060": false | ||
| } | ||
| 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). |
| 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
|
||
| using Moq.Analyzers.Common; | ||
|
|
||
| namespace Moq.Analyzers; | ||
|
|
||
| /// <summary> | ||
| /// Detects when <c>Mock<T></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
|
||
| 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; | ||
| } | ||
|
|
||
| /// <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] == ','; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.