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
127 changes: 127 additions & 0 deletions src/DataverseAnalyzer/DuplicateConstructorParameterTypeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace DataverseAnalyzer;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DuplicateConstructorParameterTypeAnalyzer : DiagnosticAnalyzer
{
private static readonly Lazy<DiagnosticDescriptor> LazyRule = new(() => new DiagnosticDescriptor(
"CT0005",
Resources.CT0005_Title,
Resources.CT0005_MessageFormat,
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: Resources.CT0005_Description));

public static DiagnosticDescriptor Rule => LazyRule.Value;

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSyntaxNodeAction(AnalyzeConstructor, SyntaxKind.ConstructorDeclaration);
context.RegisterSyntaxNodeAction(
AnalyzePrimaryConstructor,
SyntaxKind.ClassDeclaration,
SyntaxKind.StructDeclaration,
SyntaxKind.RecordDeclaration,
SyntaxKind.RecordStructDeclaration);
}

private static void AnalyzeConstructor(SyntaxNodeAnalysisContext context)
{
var constructor = (ConstructorDeclarationSyntax)context.Node;
if (constructor.ParameterList is null)
return;

AnalyzeParameterList(context, constructor.ParameterList);
}

private static void AnalyzePrimaryConstructor(SyntaxNodeAnalysisContext context)
{
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
if (typeDeclaration.ParameterList is null)
return;

AnalyzeParameterList(context, typeDeclaration.ParameterList);
}

private static void AnalyzeParameterList(SyntaxNodeAnalysisContext context, ParameterListSyntax parameterList)
{
var parameters = parameterList.Parameters;
if (parameters.Count < 2)
return;

var parametersByType = new Dictionary<ITypeSymbol, List<string>>(SymbolEqualityComparer.Default);

foreach (var parameter in parameters)
{
if (parameter.Type is null)
continue;

var typeInfo = context.SemanticModel.GetTypeInfo(parameter.Type);
var typeSymbol = typeInfo.Type;

if (typeSymbol is null)
continue;

if (!IsDependencyInjectionType(typeSymbol))
continue;

var paramName = parameter.Identifier.ValueText;

if (!parametersByType.TryGetValue(typeSymbol, out var paramNames))
{
paramNames = new List<string>();
parametersByType[typeSymbol] = paramNames;
}

paramNames.Add(paramName);
}

foreach (var kvp in parametersByType)
{
if (kvp.Value.Count < 2)
continue;

var typeSymbol = kvp.Key;
var paramNames = kvp.Value;

var typeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
var paramNamesJoined = string.Join(", ", paramNames);

var diagnostic = Diagnostic.Create(
Rule,
parameterList.GetLocation(),
typeName,
paramNamesJoined);

context.ReportDiagnostic(diagnostic);
}
}

private static bool IsDependencyInjectionType(ITypeSymbol typeSymbol)
{
var typeName = typeSymbol.Name;
return typeName.EndsWith("Service", StringComparison.Ordinal) ||
typeName.EndsWith("Repository", StringComparison.Ordinal) ||
typeName.EndsWith("Handler", StringComparison.Ordinal) ||
typeName.EndsWith("Provider", StringComparison.Ordinal) ||
typeName.EndsWith("Factory", StringComparison.Ordinal) ||
typeName.EndsWith("Manager", StringComparison.Ordinal) ||
typeName.EndsWith("Client", StringComparison.Ordinal);
}
}
6 changes: 6 additions & 0 deletions src/DataverseAnalyzer/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/DataverseAnalyzer/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,13 @@
<data name="CT0004_CodeFix_Title" xml:space="preserve">
<value>Remove braces</value>
</data>
<data name="CT0005_Title" xml:space="preserve">
<value>Constructor has duplicate dependency injection parameter types</value>
</data>
<data name="CT0005_MessageFormat" xml:space="preserve">
<value>Constructor has multiple parameters of type '{0}': {1}</value>
</data>
<data name="CT0005_Description" xml:space="preserve">
<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>
</data>
</root>
Loading