Skip to content

Commit a043c3a

Browse files
committed
Add CT0005 analyzer to detect duplicate dependency injection parameter types
1 parent 5febfe1 commit a043c3a

File tree

4 files changed

+496
-0
lines changed

4 files changed

+496
-0
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace DataverseAnalyzer;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public sealed class DuplicateConstructorParameterTypeAnalyzer : DiagnosticAnalyzer
11+
{
12+
private static readonly Lazy<DiagnosticDescriptor> LazyRule = new(() => new DiagnosticDescriptor(
13+
"CT0005",
14+
Resources.CT0005_Title,
15+
Resources.CT0005_MessageFormat,
16+
"Usage",
17+
DiagnosticSeverity.Warning,
18+
isEnabledByDefault: true,
19+
description: Resources.CT0005_Description));
20+
21+
public static DiagnosticDescriptor Rule => LazyRule.Value;
22+
23+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
24+
25+
public override void Initialize(AnalysisContext context)
26+
{
27+
if (context is null)
28+
{
29+
throw new ArgumentNullException(nameof(context));
30+
}
31+
32+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
33+
context.EnableConcurrentExecution();
34+
35+
context.RegisterSyntaxNodeAction(AnalyzeConstructor, SyntaxKind.ConstructorDeclaration);
36+
context.RegisterSyntaxNodeAction(
37+
AnalyzePrimaryConstructor,
38+
SyntaxKind.ClassDeclaration,
39+
SyntaxKind.StructDeclaration,
40+
SyntaxKind.RecordDeclaration,
41+
SyntaxKind.RecordStructDeclaration);
42+
}
43+
44+
private static void AnalyzeConstructor(SyntaxNodeAnalysisContext context)
45+
{
46+
var constructor = (ConstructorDeclarationSyntax)context.Node;
47+
if (constructor.ParameterList is null)
48+
return;
49+
50+
AnalyzeParameterList(context, constructor.ParameterList);
51+
}
52+
53+
private static void AnalyzePrimaryConstructor(SyntaxNodeAnalysisContext context)
54+
{
55+
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
56+
if (typeDeclaration.ParameterList is null)
57+
return;
58+
59+
AnalyzeParameterList(context, typeDeclaration.ParameterList);
60+
}
61+
62+
private static void AnalyzeParameterList(SyntaxNodeAnalysisContext context, ParameterListSyntax parameterList)
63+
{
64+
var parameters = parameterList.Parameters;
65+
if (parameters.Count < 2)
66+
return;
67+
68+
var parametersByType = new Dictionary<ITypeSymbol, List<string>>(SymbolEqualityComparer.Default);
69+
70+
foreach (var parameter in parameters)
71+
{
72+
if (parameter.Type is null)
73+
continue;
74+
75+
var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
76+
var typeSymbol = typeInfo.Type;
77+
78+
if (typeSymbol is null)
79+
continue;
80+
81+
if (!IsDependencyInjectionType(typeSymbol))
82+
continue;
83+
84+
var paramName = parameter.Identifier.ValueText;
85+
86+
if (!parametersByType.TryGetValue(typeSymbol, out var paramNames))
87+
{
88+
paramNames = new List<string>();
89+
parametersByType[typeSymbol] = paramNames;
90+
}
91+
92+
paramNames.Add(paramName);
93+
}
94+
95+
foreach (var kvp in parametersByType)
96+
{
97+
if (kvp.Value.Count < 2)
98+
continue;
99+
100+
var typeSymbol = kvp.Key;
101+
var paramNames = kvp.Value;
102+
103+
var typeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
104+
var paramNamesJoined = string.Join(", ", paramNames);
105+
106+
var diagnostic = Diagnostic.Create(
107+
Rule,
108+
parameterList.GetLocation(),
109+
typeName,
110+
paramNamesJoined);
111+
112+
context.ReportDiagnostic(diagnostic);
113+
}
114+
}
115+
116+
private static bool IsDependencyInjectionType(ITypeSymbol typeSymbol)
117+
{
118+
var typeName = typeSymbol.Name;
119+
return typeName.EndsWith("Service", StringComparison.Ordinal) ||
120+
typeName.EndsWith("Repository", StringComparison.Ordinal) ||
121+
typeName.EndsWith("Handler", StringComparison.Ordinal) ||
122+
typeName.EndsWith("Provider", StringComparison.Ordinal) ||
123+
typeName.EndsWith("Factory", StringComparison.Ordinal) ||
124+
typeName.EndsWith("Manager", StringComparison.Ordinal) ||
125+
typeName.EndsWith("Client", StringComparison.Ordinal);
126+
}
127+
}

src/DataverseAnalyzer/Resources.Designer.cs

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/DataverseAnalyzer/Resources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,13 @@
100100
<data name="CT0004_CodeFix_Title" xml:space="preserve">
101101
<value>Remove braces</value>
102102
</data>
103+
<data name="CT0005_Title" xml:space="preserve">
104+
<value>Constructor has duplicate dependency injection parameter types</value>
105+
</data>
106+
<data name="CT0005_MessageFormat" xml:space="preserve">
107+
<value>Constructor has multiple parameters of type '{0}': {1}</value>
108+
</data>
109+
<data name="CT0005_Description" xml:space="preserve">
110+
<value>Having multiple constructor parameters of the same dependency injection type (Service, Repository, Handler, Provider, Factory, Manager, Client) can lead to confusion and accidental parameter swapping.</value>
111+
</data>
103112
</root>

0 commit comments

Comments
 (0)