< Summary

Information
Class: NexusLabs.Needlr.Generators.GenerateFactoryAttributeAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/GenerateFactoryAttributeAnalyzer.cs
Line coverage
87%
Covered lines: 77
Uncovered lines: 11
Coverable lines: 88
Total lines: 199
Line coverage: 87.5%
Branch coverage
74%
Covered branches: 40
Total branches: 54
Branch coverage: 74%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_SupportedDiagnostics()100%11100%
Initialize(...)100%11100%
AnalyzeAttribute(...)75%212088.46%
AnalyzeConstructorParameters(...)85.71%141494.11%
IsGenerateFactoryAttribute(...)62.5%10866.66%
IsInjectableParameterType(...)66.66%151272.72%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/GenerateFactoryAttributeAnalyzer.cs

#LineLine coverage
 1using System.Collections.Immutable;
 2using System.Linq;
 3
 4using Microsoft.CodeAnalysis;
 5using Microsoft.CodeAnalysis.CSharp;
 6using Microsoft.CodeAnalysis.CSharp.Syntax;
 7using Microsoft.CodeAnalysis.Diagnostics;
 8
 9namespace NexusLabs.Needlr.Generators;
 10
 11/// <summary>
 12/// Analyzer that validates [GenerateFactory] and [GenerateFactory&lt;T&gt;] attribute usage:
 13/// - NDLRGEN003: All constructor parameters are injectable (factory unnecessary)
 14/// - NDLRGEN004: No constructor parameters are injectable (low value factory)
 15/// - NDLRGEN005: Type argument T is not an interface implemented by the class
 16/// </summary>
 17[DiagnosticAnalyzer(LanguageNames.CSharp)]
 18public sealed class GenerateFactoryAttributeAnalyzer : DiagnosticAnalyzer
 19{
 20    private const string GenerateFactoryAttributeName = "GenerateFactoryAttribute";
 21    private const string GeneratorsNamespace = "NexusLabs.Needlr.Generators";
 22
 23    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 20824        ImmutableArray.Create(
 20825            DiagnosticDescriptors.FactoryAllParamsInjectable,
 20826            DiagnosticDescriptors.FactoryNoInjectableParams,
 20827            DiagnosticDescriptors.FactoryTypeArgNotImplemented);
 28
 29    public override void Initialize(AnalysisContext context)
 30    {
 2031        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2032        context.EnableConcurrentExecution();
 33
 2034        context.RegisterSyntaxNodeAction(AnalyzeAttribute, SyntaxKind.Attribute);
 2035    }
 36
 37    private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context)
 38    {
 16839        var attributeSyntax = (AttributeSyntax)context.Node;
 16840        var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol?.ContainingType;
 41
 16842        if (attributeSymbol == null)
 043            return;
 44
 45        // Check if this is a [GenerateFactory] or [GenerateFactory<T>] attribute
 16846        if (!IsGenerateFactoryAttribute(attributeSymbol))
 15647            return;
 48
 49        // Get the class this attribute is applied to
 1250        var classDeclaration = attributeSyntax.Parent?.Parent as ClassDeclarationSyntax;
 1251        if (classDeclaration == null)
 052            return;
 53
 1254        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 1255        if (classSymbol == null)
 056            return;
 57
 58        // NDLRGEN005: Check if generic type argument is implemented by the class
 1259        if (attributeSymbol.IsGenericType && attributeSymbol.TypeArguments.Length == 1)
 60        {
 461            var typeArg = attributeSymbol.TypeArguments[0] as INamedTypeSymbol;
 462            if (typeArg != null)
 63            {
 64                // Check if the class implements this interface
 465                bool implementsInterface = classSymbol.AllInterfaces.Any(i =>
 966                    SymbolEqualityComparer.Default.Equals(i, typeArg));
 67
 468                if (!implementsInterface)
 69                {
 270                    var diagnostic = Diagnostic.Create(
 271                        DiagnosticDescriptors.FactoryTypeArgNotImplemented,
 272                        attributeSyntax.GetLocation(),
 273                        classSymbol.Name,
 274                        typeArg.Name);
 75
 276                    context.ReportDiagnostic(diagnostic);
 77                }
 78            }
 79        }
 80
 81        // Analyze constructor parameters for NDLRGEN003 and NDLRGEN004
 1282        AnalyzeConstructorParameters(context, attributeSyntax, classSymbol);
 1283    }
 84
 85    private static void AnalyzeConstructorParameters(
 86        SyntaxNodeAnalysisContext context,
 87        AttributeSyntax attributeSyntax,
 88        INamedTypeSymbol classSymbol)
 89    {
 90        // Find the best constructor (public, most parameters)
 1291        var publicCtors = classSymbol.InstanceConstructors
 1292            .Where(c => c.DeclaredAccessibility == Accessibility.Public && !c.IsStatic)
 093            .OrderByDescending(c => c.Parameters.Length)
 1294            .ToList();
 95
 1296        if (publicCtors.Count == 0)
 097            return;
 98
 99        // Check all constructors - we care about the "best" one for the warning
 12100        var bestCtor = publicCtors[0];
 12101        var parameters = bestCtor.Parameters;
 102
 12103        if (parameters.Length == 0)
 104        {
 105            // No parameters at all - factory is pointless
 2106            var diagnostic = Diagnostic.Create(
 2107                DiagnosticDescriptors.FactoryNoInjectableParams,
 2108                attributeSyntax.GetLocation(),
 2109                classSymbol.Name);
 110
 2111            context.ReportDiagnostic(diagnostic);
 2112            return;
 113        }
 114
 10115        int injectableCount = 0;
 10116        int runtimeCount = 0;
 117
 62118        foreach (var param in parameters)
 119        {
 21120            if (IsInjectableParameterType(param.Type))
 121            {
 10122                injectableCount++;
 123            }
 124            else
 125            {
 11126                runtimeCount++;
 127            }
 128        }
 129
 130        // NDLRGEN003: All params are injectable - factory unnecessary
 10131        if (runtimeCount == 0)
 132        {
 2133            var diagnostic = Diagnostic.Create(
 2134                DiagnosticDescriptors.FactoryAllParamsInjectable,
 2135                attributeSyntax.GetLocation(),
 2136                classSymbol.Name);
 137
 2138            context.ReportDiagnostic(diagnostic);
 139        }
 140        // NDLRGEN004: No params are injectable - low value factory
 8141        else if (injectableCount == 0)
 142        {
 2143            var diagnostic = Diagnostic.Create(
 2144                DiagnosticDescriptors.FactoryNoInjectableParams,
 2145                attributeSyntax.GetLocation(),
 2146                classSymbol.Name);
 147
 2148            context.ReportDiagnostic(diagnostic);
 149        }
 8150    }
 151
 152    private static bool IsGenerateFactoryAttribute(INamedTypeSymbol attributeSymbol)
 153    {
 154        // Handle both GenerateFactoryAttribute and GenerateFactoryAttribute<T>
 168155        var name = attributeSymbol.Name;
 168156        if (name != GenerateFactoryAttributeName)
 157        {
 158            // Check original definition for generic case
 156159            if (attributeSymbol.IsGenericType)
 160            {
 0161                name = attributeSymbol.OriginalDefinition.Name;
 0162                if (name != GenerateFactoryAttributeName)
 0163                    return false;
 164            }
 165            else
 166            {
 156167                return false;
 168            }
 169        }
 170
 12171        var ns = attributeSymbol.ContainingNamespace?.ToString();
 12172        return ns == GeneratorsNamespace;
 173    }
 174
 175    private static bool IsInjectableParameterType(ITypeSymbol typeSymbol)
 176    {
 177        // Value types (int, string, DateTime, Guid, etc.) are NOT injectable
 21178        if (typeSymbol.IsValueType)
 3179            return false;
 180
 181        // String is special - it's a reference type but not injectable
 18182        if (typeSymbol.SpecialType == SpecialType.System_String)
 8183            return false;
 184
 185        // Delegates are not injectable
 10186        if (typeSymbol.TypeKind == TypeKind.Delegate)
 0187            return false;
 188
 189        // Arrays are typically not injectable by DI
 10190        if (typeSymbol.TypeKind == TypeKind.Array)
 0191            return false;
 192
 193        // Interfaces and classes are injectable
 10194        if (typeSymbol.TypeKind == TypeKind.Interface || typeSymbol.TypeKind == TypeKind.Class)
 10195            return true;
 196
 0197        return false;
 198    }
 199}