diff --git a/Directory.Packages.props b/Directory.Packages.props
index 80334e13..672bc235 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,6 +3,7 @@
true
true
+
diff --git a/Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs b/Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs
index 34fcf7b3..d1edfa2a 100644
--- a/Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs
+++ b/Source/Moq.Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs
@@ -3,6 +3,8 @@ namespace Moq.Analyzers;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsShouldBeUsedOnlyForInterfaceAnalyzer : DiagnosticAnalyzer
{
+ private static readonly MoqMethodDescriptorBase MoqAsMethodDescriptor = new MoqAsMethodDescriptor();
+
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
Diagnostics.AsShouldBeUsedOnlyForInterfaceId,
Diagnostics.AsShouldBeUsedOnlyForInterfaceTitle,
@@ -22,20 +24,37 @@ public override void Initialize(AnalysisContext context)
private static void Analyze(SyntaxNodeAnalysisContext context)
{
- InvocationExpressionSyntax? asInvocation = (InvocationExpressionSyntax)context.Node;
+ if (context.Node is not InvocationExpressionSyntax invocationExpression)
+ {
+ return;
+ }
+
+ if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessSyntax)
+ {
+ return;
+ }
+
+ if (!MoqAsMethodDescriptor.IsMatch(context.SemanticModel, memberAccessSyntax, context.CancellationToken))
+ {
+ return;
+ }
+
+ if (!memberAccessSyntax.Name.TryGetGenericArguments(out SeparatedSyntaxList typeArguments))
+ {
+ return;
+ }
+
+ if (typeArguments.Count != 1)
+ {
+ return;
+ }
+
+ TypeSyntax typeArgument = typeArguments[0];
+ SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(typeArgument, context.CancellationToken);
- if (asInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression
- && Helpers.IsMoqAsMethod(context.SemanticModel, memberAccessExpression)
- && memberAccessExpression.Name is GenericNameSyntax genericName
- && genericName.TypeArgumentList.Arguments.Count == 1)
+ if (symbolInfo.Symbol is ITypeSymbol { TypeKind: not TypeKind.Interface })
{
- TypeSyntax? typeArgument = genericName.TypeArgumentList.Arguments[0];
- SymbolInfo symbolInfo = context.SemanticModel.GetSymbolInfo(typeArgument, context.CancellationToken);
- if (symbolInfo.Symbol is ITypeSymbol typeSymbol && typeSymbol.TypeKind != TypeKind.Interface)
- {
- Diagnostic? diagnostic = Diagnostic.Create(Rule, typeArgument.GetLocation());
- context.ReportDiagnostic(diagnostic);
- }
+ context.ReportDiagnostic(Diagnostic.Create(Rule, typeArgument.GetLocation()));
}
}
}
diff --git a/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs b/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs
index da27c8af..2b294dae 100644
--- a/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs
+++ b/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodAnalyzer.cs
@@ -43,7 +43,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
SeparatedSyntaxList lambdaParameters = callbackLambda.ParameterList.Parameters;
if (lambdaParameters.Count == 0) return;
- InvocationExpressionSyntax? setupInvocation = Helpers.FindSetupMethodFromCallbackInvocation(context.SemanticModel, callbackOrReturnsInvocation);
+ InvocationExpressionSyntax? setupInvocation = Helpers.FindSetupMethodFromCallbackInvocation(context.SemanticModel, callbackOrReturnsInvocation, context.CancellationToken);
InvocationExpressionSyntax? mockedMethodInvocation = Helpers.FindMockedMethodInvocationFromSetupMethod(setupInvocation);
if (mockedMethodInvocation == null) return;
diff --git a/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs b/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs
index d211dd98..5e88c879 100644
--- a/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs
+++ b/Source/Moq.Analyzers/CallbackSignatureShouldMatchMockedMethodCodeFix.cs
@@ -64,7 +64,7 @@ private async Task FixCallbackSignatureAsync(SyntaxNode root, Document
return document;
}
- InvocationExpressionSyntax? setupMethodInvocation = Helpers.FindSetupMethodFromCallbackInvocation(semanticModel, callbackInvocation);
+ InvocationExpressionSyntax? setupMethodInvocation = Helpers.FindSetupMethodFromCallbackInvocation(semanticModel, callbackInvocation, cancellationToken);
Debug.Assert(setupMethodInvocation != null, nameof(setupMethodInvocation) + " != null");
IMethodSymbol[]? matchingMockedMethods = Helpers.GetAllMatchingMockedMethodSymbolsFromSetupMethodInvocation(semanticModel, setupMethodInvocation).ToArray();
diff --git a/Source/Moq.Analyzers/Helpers.cs b/Source/Moq.Analyzers/Helpers.cs
index b942cc1c..ab0165c1 100644
--- a/Source/Moq.Analyzers/Helpers.cs
+++ b/Source/Moq.Analyzers/Helpers.cs
@@ -1,22 +1,14 @@
using System.Diagnostics;
-using System.Text.RegularExpressions;
namespace Moq.Analyzers;
internal static class Helpers
{
- private static readonly MoqMethodDescriptor MoqSetupMethodDescriptor = new("Setup", new Regex("^Moq\\.Mock<.*>\\.Setup\\.*"));
+ private static readonly MoqMethodDescriptorBase MoqSetupMethodDescriptor = new MoqSetupMethodDescriptor();
- private static readonly MoqMethodDescriptor MoqAsMethodDescriptor = new("As", new Regex("^Moq\\.Mock\\.As<\\.*"), isGeneric: true);
-
- internal static bool IsMoqSetupMethod(SemanticModel semanticModel, MemberAccessExpressionSyntax method)
- {
- return MoqSetupMethodDescriptor.IsMoqMethod(semanticModel, method);
- }
-
- internal static bool IsMoqAsMethod(SemanticModel semanticModel, MemberAccessExpressionSyntax method)
+ internal static bool IsMoqSetupMethod(SemanticModel semanticModel, MemberAccessExpressionSyntax method, CancellationToken cancellationToken)
{
- return MoqAsMethodDescriptor.IsMoqMethod(semanticModel, method);
+ return MoqSetupMethodDescriptor.IsMatch(semanticModel, method, cancellationToken);
}
internal static bool IsCallbackOrReturnInvocation(SemanticModel semanticModel, InvocationExpressionSyntax callbackOrReturnsInvocation)
@@ -48,12 +40,12 @@ internal static bool IsCallbackOrReturnInvocation(SemanticModel semanticModel, I
};
}
- internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation(SemanticModel semanticModel, ExpressionSyntax expression)
+ internal static InvocationExpressionSyntax? FindSetupMethodFromCallbackInvocation(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
{
InvocationExpressionSyntax? invocation = expression as InvocationExpressionSyntax;
if (invocation?.Expression is not MemberAccessExpressionSyntax method) return null;
- if (IsMoqSetupMethod(semanticModel, method)) return invocation;
- return FindSetupMethodFromCallbackInvocation(semanticModel, method.Expression);
+ if (IsMoqSetupMethod(semanticModel, method, cancellationToken)) return invocation;
+ return FindSetupMethodFromCallbackInvocation(semanticModel, method.Expression, cancellationToken);
}
internal static InvocationExpressionSyntax? FindMockedMethodInvocationFromSetupMethod(InvocationExpressionSyntax? setupInvocation)
diff --git a/Source/Moq.Analyzers/MoqAsMethodDescriptor.cs b/Source/Moq.Analyzers/MoqAsMethodDescriptor.cs
new file mode 100644
index 00000000..afe70d77
--- /dev/null
+++ b/Source/Moq.Analyzers/MoqAsMethodDescriptor.cs
@@ -0,0 +1,32 @@
+namespace Moq.Analyzers;
+
+///
+/// A class that, given a and a , determines if
+/// it is a call to the Moq `Mock.As()` method.
+///
+internal class MoqAsMethodDescriptor : MoqMethodDescriptorBase
+{
+ private const string MethodName = "As";
+
+ public override bool IsMatch(SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessSyntax, CancellationToken cancellationToken)
+ {
+ if (!IsFastMatch(memberAccessSyntax, MethodName.AsSpan()))
+ {
+ return false;
+ }
+
+ ISymbol? symbol = semanticModel.GetSymbolInfo(memberAccessSyntax, cancellationToken).Symbol;
+
+ if (symbol is not IMethodSymbol methodSymbol)
+ {
+ return false;
+ }
+
+ if (!IsContainedInMockType(methodSymbol))
+ {
+ return false;
+ }
+
+ return methodSymbol.Name.AsSpan().SequenceEqual(MethodName.AsSpan()) && methodSymbol.IsGenericMethod;
+ }
+}
diff --git a/Source/Moq.Analyzers/MoqMethodDescriptor.cs b/Source/Moq.Analyzers/MoqMethodDescriptor.cs
deleted file mode 100644
index 70289697..00000000
--- a/Source/Moq.Analyzers/MoqMethodDescriptor.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Diagnostics;
-using System.Text.RegularExpressions;
-
-namespace Moq.Analyzers;
-
-internal class MoqMethodDescriptor
-{
- private readonly bool _isGeneric;
-
- public MoqMethodDescriptor(string shortMethodName, Regex fullMethodNamePattern, bool isGeneric = false)
- {
- _isGeneric = isGeneric;
- ShortMethodName = shortMethodName;
- FullMethodNamePattern = fullMethodNamePattern;
- }
-
- private string ShortMethodName { get; }
-
- private Regex FullMethodNamePattern { get; }
-
- public bool IsMoqMethod(SemanticModel semanticModel, MemberAccessExpressionSyntax? method)
- {
- string? methodName = method?.Name.ToString();
-
- Debug.Assert(!string.IsNullOrEmpty(methodName), nameof(methodName) + " != null or empty");
-
- if (string.IsNullOrEmpty(methodName)) return false;
-
- // First fast check before walking semantic model
- if (!DoesShortMethodMatch(methodName!)) return false;
-
- Debug.Assert(method != null, nameof(method) + " != null");
-
- if (method == null)
- {
- return false;
- }
-
- SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(method);
- return symbolInfo.CandidateReason switch
- {
- CandidateReason.OverloadResolutionFailure => symbolInfo.CandidateSymbols.OfType().Any(s => FullMethodNamePattern.IsMatch(s.ToString())),
- CandidateReason.None => symbolInfo.Symbol is IMethodSymbol &&
- FullMethodNamePattern.IsMatch(symbolInfo.Symbol.ToString()),
- _ => false,
- };
- }
-
- private bool DoesShortMethodMatch(string methodName)
- {
- if (_isGeneric)
- {
- return methodName.StartsWith($"{ShortMethodName}<", StringComparison.Ordinal);
- }
-
- return string.Equals(methodName, ShortMethodName, StringComparison.Ordinal);
- }
-}
diff --git a/Source/Moq.Analyzers/MoqMethodDescriptorBase.cs b/Source/Moq.Analyzers/MoqMethodDescriptorBase.cs
new file mode 100644
index 00000000..cc8bc090
--- /dev/null
+++ b/Source/Moq.Analyzers/MoqMethodDescriptorBase.cs
@@ -0,0 +1,38 @@
+namespace Moq.Analyzers;
+
+///
+/// A base that that provides common functionality for identifying if a given
+/// is a specific Moq method.
+///
+///
+/// Currently the abstract method
+/// is specific to because that's the only type of syntax in use. I expect we'll need
+/// to loosen this restriction if we start using other types of syntax.
+///
+internal abstract class MoqMethodDescriptorBase
+{
+ private const string ContainingNamespace = "Moq";
+ private const string ContainingType = "Mock";
+
+ public abstract bool IsMatch(SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessSyntax, CancellationToken cancellationToken);
+
+ protected static bool IsFastMatch(MemberAccessExpressionSyntax memberAccessSyntax, ReadOnlySpan methodName)
+ {
+ return memberAccessSyntax.Name.Identifier.Text.AsSpan().SequenceEqual(methodName);
+ }
+
+ protected static bool IsContainedInMockType(IMethodSymbol methodSymbol)
+ {
+ return IsInMoqNamespace(methodSymbol) && IsInMockType(methodSymbol);
+ }
+
+ private static bool IsInMoqNamespace(ISymbol symbol)
+ {
+ return symbol.ContainingNamespace.Name.AsSpan().SequenceEqual(ContainingNamespace.AsSpan());
+ }
+
+ private static bool IsInMockType(ISymbol symbol)
+ {
+ return symbol.ContainingType.Name.AsSpan().SequenceEqual(ContainingType.AsSpan());
+ }
+}
diff --git a/Source/Moq.Analyzers/MoqSetupMethodDescriptor.cs b/Source/Moq.Analyzers/MoqSetupMethodDescriptor.cs
new file mode 100644
index 00000000..fe42697b
--- /dev/null
+++ b/Source/Moq.Analyzers/MoqSetupMethodDescriptor.cs
@@ -0,0 +1,32 @@
+namespace Moq.Analyzers;
+
+///
+/// A class that, given a and a , determines if
+/// it is a call to the Moq `Mock.Setup()` method.
+///
+internal class MoqSetupMethodDescriptor : MoqMethodDescriptorBase
+{
+ private const string MethodName = "Setup";
+
+ public override bool IsMatch(SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccessSyntax, CancellationToken cancellationToken)
+ {
+ if (!IsFastMatch(memberAccessSyntax, MethodName.AsSpan()))
+ {
+ return false;
+ }
+
+ ISymbol? symbol = semanticModel.GetSymbolInfo(memberAccessSyntax, cancellationToken).Symbol;
+
+ if (symbol is not IMethodSymbol methodSymbol)
+ {
+ return false;
+ }
+
+ if (!IsContainedInMockType(methodSymbol))
+ {
+ return false;
+ }
+
+ return methodSymbol.Name.AsSpan().SequenceEqual(MethodName.AsSpan()) && methodSymbol.IsGenericMethod;
+ }
+}
diff --git a/Source/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/Source/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs
index d829e24c..d6971626 100644
--- a/Source/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs
+++ b/Source/Moq.Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs
@@ -24,7 +24,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
InvocationExpressionSyntax? setupInvocation = (InvocationExpressionSyntax)context.Node;
- if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression))
+ if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression, context.CancellationToken))
{
ExpressionSyntax? mockedMemberExpression = Helpers.FindMockedMemberExpressionFromSetupMethod(setupInvocation);
if (mockedMemberExpression == null)
diff --git a/Source/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs b/Source/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs
index 25b81115..cc294de5 100644
--- a/Source/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs
+++ b/Source/Moq.Analyzers/SetupShouldNotIncludeAsyncResultAnalyzer.cs
@@ -24,7 +24,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
InvocationExpressionSyntax? setupInvocation = (InvocationExpressionSyntax)context.Node;
- if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression))
+ if (setupInvocation.Expression is MemberAccessExpressionSyntax memberAccessExpression && Helpers.IsMoqSetupMethod(context.SemanticModel, memberAccessExpression, context.CancellationToken))
{
ExpressionSyntax? mockedMemberExpression = Helpers.FindMockedMemberExpressionFromSetupMethod(setupInvocation);
if (mockedMemberExpression == null)
diff --git a/Source/Moq.Analyzers/SyntaxExtensions.cs b/Source/Moq.Analyzers/SyntaxExtensions.cs
new file mode 100644
index 00000000..9099f12e
--- /dev/null
+++ b/Source/Moq.Analyzers/SyntaxExtensions.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Moq.Analyzers;
+
+///
+/// Extensions methods for s.
+///
+internal static class SyntaxExtensions
+{
+ ///
+ /// Tries to get the generic arguments of a given .
+ ///
+ /// The syntax to inspect.
+ /// The collection of elements on the .
+ /// if has generic / type parameters; otherwise.
+ ///
+ /// x.As<ISampleInterface>() returns and will contain ISampleInterface.
+ ///
+ ///
+ /// x.As() returns and will be empty.
+ ///
+ public static bool TryGetGenericArguments(this NameSyntax syntax, [NotNullWhen(true)] out SeparatedSyntaxList typeArguments)
+ {
+ if (syntax is GenericNameSyntax genericName)
+ {
+ typeArguments = genericName.TypeArgumentList.Arguments;
+ return true;
+ }
+
+ typeArguments = default;
+ return false;
+ }
+}
diff --git a/build/targets/compiler/Compiler.props b/build/targets/compiler/Compiler.props
index 2cab0aac..7f9eb03a 100644
--- a/build/targets/compiler/Compiler.props
+++ b/build/targets/compiler/Compiler.props
@@ -4,4 +4,11 @@
12.0
enable
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
diff --git a/build/targets/compiler/Packages.props b/build/targets/compiler/Packages.props
new file mode 100644
index 00000000..e1375feb
--- /dev/null
+++ b/build/targets/compiler/Packages.props
@@ -0,0 +1,5 @@
+
+
+
+
+