-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathAsShouldBeUsedOnlyForInterfaceAnalyzer.cs
More file actions
132 lines (113 loc) · 5.57 KB
/
Copy pathAsShouldBeUsedOnlyForInterfaceAnalyzer.cs
File metadata and controls
132 lines (113 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
using System.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Moq.Analyzers;
/// <summary>
/// Mock.As() should take interfaces only.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsShouldBeUsedOnlyForInterfaceAnalyzer : MoqDiagnosticAnalyzerBase
{
private static readonly LocalizableString Title = "Moq: Invalid As type parameter";
private static readonly LocalizableString Message = "Mock.As() should take interfaces only, but '{0}' is not an interface";
private static readonly LocalizableString Description = "Mock.As() should take interfaces only.";
private static readonly DiagnosticDescriptor Rule = new(
DiagnosticIds.AsShouldOnlyBeUsedForInterfacesRuleId,
Title,
Message,
DiagnosticCategory.Usage,
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: Description,
helpLinkUri: $"https://github.com/rjmurillo/moq.analyzers/blob/{ThisAssembly.GitCommitId}/docs/rules/{DiagnosticIds.AsShouldOnlyBeUsedForInterfacesRuleId}.md");
/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);
private protected override void RegisterCompilationActions(CompilationStartAnalysisContext context, MoqKnownSymbols knownSymbols)
{
// Look for the Mock.As() method and provide it to Analyze to avoid looking it up multiple times.
ImmutableArray<IMethodSymbol> asMethods = ImmutableArray.CreateRange([
..knownSymbols.MockAs,
..knownSymbols.Mock1As]);
// If As() methods are not available, this may indicate an unsupported Moq version
if (asMethods.IsEmpty)
{
return;
}
context.RegisterOperationAction(
operationAnalysisContext => Analyze(operationAnalysisContext, asMethods),
OperationKind.Invocation);
}
private static void Analyze(OperationAnalysisContext context, ImmutableArray<IMethodSymbol> wellKnownAsMethods)
{
// This should always be an invocation operation since we registered for OperationKind.Invocation
Debug.Assert(context.Operation is IInvocationOperation, "Expected IInvocationOperation");
if (context.Operation is not IInvocationOperation invocationOperation)
{
return;
}
IMethodSymbol targetMethod = invocationOperation.TargetMethod;
if (!targetMethod.IsInstanceOf(wellKnownAsMethods))
{
return;
}
ImmutableArray<ITypeSymbol> typeArguments = targetMethod.TypeArguments;
if (typeArguments.Length != 1)
{
return;
}
ITypeSymbol typeSymbol = typeArguments[0];
// Interface: this is the valid, intended use of As<T>, so never report.
// Error: the type failed to bind, so the code already has a compiler error; adding
// Moq1300 on top is noise (issue #1251).
if (typeSymbol.TypeKind is TypeKind.Interface or TypeKind.Error)
{
return;
}
// Open generic type parameter: at the call site T may be substituted with an interface,
// so reporting is a false positive (issue #1251) UNLESS its constraints make an interface
// substitution impossible (a value-type or base-class constraint). In that case As<T> can
// never bind to an interface, so the diagnostic is correct.
if (typeSymbol is ITypeParameterSymbol typeParameter && CanBeSubstitutedWithInterface(typeParameter))
{
return;
}
// Find the first As<T> generic type argument and report the diagnostic on it
GenericNameSyntax? asGeneric = invocationOperation.Syntax
.DescendantNodes()
.OfType<GenericNameSyntax>()
.FirstOrDefault(x => string.Equals(x.Identifier.ValueText, "As", StringComparison.Ordinal));
TypeSyntax? typeArg = asGeneric?.TypeArgumentList.Arguments.FirstOrDefault();
Location location = typeArg?.GetLocation() ?? invocationOperation.Syntax.GetLocation();
context.ReportDiagnostic(location.CreateDiagnostic(Rule, typeSymbol.ToDisplayString()));
}
/// <summary>
/// Determines whether an open generic type parameter could be substituted with an interface
/// at a call site, given its declared constraints.
/// </summary>
/// <param name="typeParameter">The open generic type parameter used as the <c>As<T></c> argument.</param>
/// <returns>
/// <see langword="false" /> when a value-type constraint (<c>struct</c>/<c>unmanaged</c>), a
/// constructor constraint (<c>new()</c>), or a base-class constraint forbids an interface
/// substitution; otherwise <see langword="true" />.
/// </returns>
private static bool CanBeSubstitutedWithInterface(ITypeParameterSymbol typeParameter)
{
// A value type can never be an interface. A `new()` constraint requires a public
// parameterless constructor, which no interface can satisfy, so it also rules out
// an interface substitution.
if (typeParameter.HasValueTypeConstraint
|| typeParameter.HasUnmanagedTypeConstraint
|| typeParameter.HasConstructorConstraint)
{
return false;
}
// A base-class constraint forces T to derive from a class, so it cannot be an interface.
foreach (ITypeSymbol constraintType in typeParameter.ConstraintTypes)
{
if (constraintType.TypeKind == TypeKind.Class)
{
return false;
}
}
return true;
}
}