< 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
100%
Covered lines: 47
Uncovered lines: 0
Coverable lines: 47
Total lines: 139
Line coverage: 100%
Branch coverage
92%
Covered branches: 26
Total branches: 28
Branch coverage: 92.8%
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(...)100%1212100%
IsPluginInterface(...)75%44100%

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 =>
 52924        ImmutableArray.Create(DiagnosticDescriptors.PluginHasConstructorDependencies);
 25
 26    public override void Initialize(AnalysisContext context)
 27    {
 4228        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 4229        context.EnableConcurrentExecution();
 30
 4231        context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration);
 4232    }
 33
 34    private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
 35    {
 2736        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 37
 38        // Skip abstract classes
 2739        if (classDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword))
 40        {
 341            return;
 42        }
 43
 44        // Check if class implements a plugin interface
 2445        if (!ImplementsPluginInterface(classDeclaration, context.SemanticModel))
 46        {
 547            return;
 48        }
 49
 50        // Check constructors
 1951        var constructors = classDeclaration.Members
 1952            .OfType<ConstructorDeclarationSyntax>()
 2153            .Where(c => !c.Modifiers.Any(SyntaxKind.StaticKeyword))
 1954            .ToList();
 55
 56        // If no explicit constructors, there's an implicit parameterless constructor - OK
 1957        if (constructors.Count == 0)
 58        {
 159            return;
 60        }
 61
 62        // Check if there's at least one public parameterless constructor
 1863        var hasParameterlessConstructor = constructors.Any(c =>
 3864            c.Modifiers.Any(SyntaxKind.PublicKeyword) &&
 3865            c.ParameterList.Parameters.Count == 0);
 66
 1867        if (hasParameterlessConstructor)
 68        {
 269            return;
 70        }
 71
 72        // Report diagnostic on any constructor with parameters
 8673        foreach (var constructor in constructors.Where(c => c.ParameterList.Parameters.Count > 0))
 74        {
 1875            var diagnostic = Diagnostic.Create(
 1876                DiagnosticDescriptors.PluginHasConstructorDependencies,
 1877                constructor.Identifier.GetLocation(),
 1878                classDeclaration.Identifier.Text);
 79
 1880            context.ReportDiagnostic(diagnostic);
 81        }
 1682    }
 83
 84    /// <summary>
 85    /// True when <paramref name="classDeclaration"/> lists a Needlr plugin interface,
 86    /// directly or through any base type or inherited interface.
 87    /// </summary>
 88    /// <remarks>
 89    /// Matching is exclusively symbol based: a base-list entry the semantic model cannot
 90    /// resolve to a type at all is skipped rather than matched on its simple name, since
 91    /// a bare <c>IServiceCollectionPlugin</c> identifier in incomplete code may just as
 92    /// easily refer to an unrelated same-named interface from another namespace. An
 93    /// unresolved but namespace-qualified entry (an error symbol still carrying the
 94    /// <c>NexusLabs.Needlr</c> namespace and a plugin interface name) is unambiguous and
 95    /// is still diagnosed.
 96    /// </remarks>
 97    private static bool ImplementsPluginInterface(
 98        ClassDeclarationSyntax classDeclaration,
 99        SemanticModel semanticModel)
 100    {
 24101        if (classDeclaration.BaseList == null)
 102        {
 2103            return false;
 104        }
 105
 73106        foreach (var baseType in classDeclaration.BaseList.Types)
 107        {
 24108            var typeInfo = semanticModel.GetTypeInfo(baseType.Type);
 24109            var typeSymbol = typeInfo.Type;
 110
 24111            if (typeSymbol == null)
 112            {
 113                continue;
 114            }
 115
 116            // Check the type and all its interfaces
 24117            if (IsPluginInterface(typeSymbol))
 118            {
 15119                return true;
 120            }
 121
 22122            foreach (var iface in typeSymbol.AllInterfaces)
 123            {
 4124                if (IsPluginInterface(iface))
 125                {
 4126                    return true;
 127                }
 128            }
 129        }
 130
 3131        return false;
 132    }
 133
 134    private static bool IsPluginInterface(ITypeSymbol typeSymbol)
 135    {
 28136        return PluginInterfaceNames.Contains(typeSymbol.Name) &&
 28137               typeSymbol.ContainingNamespace?.ToString() == "NexusLabs.Needlr";
 138    }
 139}