< Summary

Information
Class: NexusLabs.Needlr.Analyzers.PluginConstructorDependenciesAnalyzer
Assembly: NexusLabs.Needlr.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/PluginConstructorDependenciesAnalyzer.cs
Line coverage
76%
Covered lines: 43
Uncovered lines: 13
Coverable lines: 56
Total lines: 142
Line coverage: 76.7%
Branch coverage
58%
Covered branches: 21
Total branches: 36
Branch coverage: 58.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
get_SupportedDiagnostics()100%11100%
Initialize(...)100%11100%
AnalyzeClassDeclaration(...)91.66%1212100%
ImplementsPluginInterface(...)50%421653.33%
IsPluginInterface(...)50%44100%
GetTypeName(...)0%2040%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/PluginConstructorDependenciesAnalyzer.cs

#LineLine coverage
 1using System.Collections.Immutable;
 2
 3using Microsoft.CodeAnalysis;
 4using Microsoft.CodeAnalysis.CSharp;
 5using Microsoft.CodeAnalysis.CSharp.Syntax;
 6using Microsoft.CodeAnalysis.Diagnostics;
 7
 8namespace NexusLabs.Needlr.Analyzers;
 9
 10/// <summary>
 11/// Analyzer that detects plugin implementations with constructor dependencies.
 12/// </summary>
 13[DiagnosticAnalyzer(LanguageNames.CSharp)]
 14public sealed class PluginConstructorDependenciesAnalyzer : DiagnosticAnalyzer
 15{
 16    // Plugin interfaces that are instantiated before DI is available
 117    private static readonly ImmutableHashSet<string> PluginInterfaceNames = ImmutableHashSet.Create(
 118        "IServiceCollectionPlugin",
 119        "IPostBuildServiceCollectionPlugin",
 120        "IWebApplicationBuilderPlugin",
 121        "IHostApplicationBuilderPlugin");
 22
 23    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 21324        ImmutableArray.Create(DiagnosticDescriptors.PluginHasConstructorDependencies);
 25
 26    public override void Initialize(AnalysisContext context)
 27    {
 1928        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 1929        context.EnableConcurrentExecution();
 30
 1931        context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration);
 1932    }
 33
 34    private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
 35    {
 1136        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 37
 38        // Skip abstract classes
 1139        if (classDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword))
 40        {
 141            return;
 42        }
 43
 44        // Check if class implements a plugin interface
 1045        if (!ImplementsPluginInterface(classDeclaration, context.SemanticModel))
 46        {
 147            return;
 48        }
 49
 50        // Check constructors
 951        var constructors = classDeclaration.Members
 952            .OfType<ConstructorDeclarationSyntax>()
 1153            .Where(c => !c.Modifiers.Any(SyntaxKind.StaticKeyword))
 954            .ToList();
 55
 56        // If no explicit constructors, there's an implicit parameterless constructor - OK
 957        if (constructors.Count == 0)
 58        {
 159            return;
 60        }
 61
 62        // Check if there's at least one public parameterless constructor
 863        var hasParameterlessConstructor = constructors.Any(c =>
 1864            c.Modifiers.Any(SyntaxKind.PublicKeyword) &&
 1865            c.ParameterList.Parameters.Count == 0);
 66
 867        if (hasParameterlessConstructor)
 68        {
 269            return;
 70        }
 71
 72        // Report diagnostic on any constructor with parameters
 3673        foreach (var constructor in constructors.Where(c => c.ParameterList.Parameters.Count > 0))
 74        {
 875            var diagnostic = Diagnostic.Create(
 876                DiagnosticDescriptors.PluginHasConstructorDependencies,
 877                constructor.Identifier.GetLocation(),
 878                classDeclaration.Identifier.Text);
 79
 880            context.ReportDiagnostic(diagnostic);
 81        }
 682    }
 83
 84    private static bool ImplementsPluginInterface(
 85        ClassDeclarationSyntax classDeclaration,
 86        SemanticModel semanticModel)
 87    {
 1088        if (classDeclaration.BaseList == null)
 89        {
 190            return false;
 91        }
 92
 2793        foreach (var baseType in classDeclaration.BaseList.Types)
 94        {
 995            var typeInfo = semanticModel.GetTypeInfo(baseType.Type);
 996            var typeSymbol = typeInfo.Type;
 97
 998            if (typeSymbol == null)
 99            {
 100                // Fallback to syntax-based check
 0101                var typeName = GetTypeName(baseType.Type);
 0102                if (typeName != null && PluginInterfaceNames.Contains(typeName))
 103                {
 0104                    return true;
 105                }
 106                continue;
 107            }
 108
 109            // Check the type and all its interfaces
 9110            if (IsPluginInterface(typeSymbol))
 111            {
 9112                return true;
 113            }
 114
 0115            foreach (var iface in typeSymbol.AllInterfaces)
 116            {
 0117                if (IsPluginInterface(iface))
 118                {
 0119                    return true;
 120                }
 121            }
 122        }
 123
 0124        return false;
 125    }
 126
 127    private static bool IsPluginInterface(ITypeSymbol typeSymbol)
 128    {
 9129        return PluginInterfaceNames.Contains(typeSymbol.Name) &&
 9130               typeSymbol.ContainingNamespace?.ToString() == "NexusLabs.Needlr";
 131    }
 132
 133    private static string? GetTypeName(TypeSyntax typeSyntax)
 134    {
 0135        return typeSyntax switch
 0136        {
 0137            IdentifierNameSyntax identifier => identifier.Identifier.Text,
 0138            QualifiedNameSyntax qualified => qualified.Right.Identifier.Text,
 0139            _ => null
 0140        };
 141    }
 142}