< Summary

Information
Class: NexusLabs.Needlr.Generators.GenerateFactoryAttributeAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /actions-runner/_work/needlr/needlr/src/NexusLabs.Needlr.Generators/GenerateFactoryAttributeAnalyzer.cs
Line coverage
87%
Covered lines: 83
Uncovered lines: 12
Coverable lines: 95
Total lines: 231
Line coverage: 87.3%
Branch coverage
75%
Covered branches: 45
Total branches: 60
Branch coverage: 75%
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(...)92.85%141496.55%
TryGetGeneratedConstructorParameterTypes(...)100%22100%
GetBestExplicitConstructorParameterTypes(...)50%4471.42%
IsGenerateFactoryAttribute(...)62.5%10866.66%
IsInjectableParameterType(...)66.66%151272.72%

File(s)

/actions-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
 9using NexusLabs.Needlr.Roslyn.Shared;
 10
 11namespace NexusLabs.Needlr.Generators;
 12
 13/// <summary>
 14/// Analyzer that validates [GenerateFactory] and [GenerateFactory&lt;T&gt;] attribute usage:
 15/// - NDLRGEN003: All constructor parameters are injectable (factory unnecessary)
 16/// - NDLRGEN004: No constructor parameters are injectable (low value factory)
 17/// - NDLRGEN005: Type argument T is not an interface implemented by the class
 18/// </summary>
 19[DiagnosticAnalyzer(LanguageNames.CSharp)]
 20public sealed class GenerateFactoryAttributeAnalyzer : DiagnosticAnalyzer
 21{
 22    private const string GenerateFactoryAttributeName = "GenerateFactoryAttribute";
 23    private const string GeneratorsNamespace = "NexusLabs.Needlr.Generators";
 24
 25    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 30826        ImmutableArray.Create(
 30827            DiagnosticDescriptors.FactoryAllParamsInjectable,
 30828            DiagnosticDescriptors.FactoryNoInjectableParams,
 30829            DiagnosticDescriptors.FactoryTypeArgNotImplemented);
 30
 31    public override void Initialize(AnalysisContext context)
 32    {
 2833        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2834        context.EnableConcurrentExecution();
 35
 2836        context.RegisterSyntaxNodeAction(AnalyzeAttribute, SyntaxKind.Attribute);
 2837    }
 38
 39    private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context)
 40    {
 26841        var attributeSyntax = (AttributeSyntax)context.Node;
 26842        var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol?.ContainingType;
 43
 26844        if (attributeSymbol == null)
 045            return;
 46
 47        // Check if this is a [GenerateFactory] or [GenerateFactory<T>] attribute
 26848        if (!IsGenerateFactoryAttribute(attributeSymbol))
 25149            return;
 50
 51        // Get the class this attribute is applied to
 1752        var classDeclaration = attributeSyntax.Parent?.Parent as ClassDeclarationSyntax;
 1753        if (classDeclaration == null)
 054            return;
 55
 1756        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 1757        if (classSymbol == null)
 058            return;
 59
 60        // NDLRGEN005: Check if generic type argument is implemented by the class
 1761        if (attributeSymbol.IsGenericType && attributeSymbol.TypeArguments.Length == 1)
 62        {
 463            var typeArg = attributeSymbol.TypeArguments[0] as INamedTypeSymbol;
 464            if (typeArg != null)
 65            {
 66                // Check if the class implements this interface
 467                bool implementsInterface = classSymbol.AllInterfaces.Any(i =>
 968                    SymbolEqualityComparer.Default.Equals(i, typeArg));
 69
 470                if (!implementsInterface)
 71                {
 272                    var diagnostic = Diagnostic.Create(
 273                        DiagnosticDescriptors.FactoryTypeArgNotImplemented,
 274                        attributeSyntax.GetLocation(),
 275                        classSymbol.Name,
 276                        typeArg.Name);
 77
 278                    context.ReportDiagnostic(diagnostic);
 79                }
 80            }
 81        }
 82
 83        // Analyze constructor parameters for NDLRGEN003 and NDLRGEN004
 1784        AnalyzeConstructorParameters(context, attributeSyntax, classSymbol);
 1785    }
 86
 87    private static void AnalyzeConstructorParameters(
 88        SyntaxNodeAnalysisContext context,
 89        AttributeSyntax attributeSyntax,
 90        INamedTypeSymbol classSymbol)
 91    {
 1792        var parameterTypes = TryGetGeneratedConstructorParameterTypes(classSymbol) ?? GetBestExplicitConstructorParamete
 1793        if (parameterTypes is null)
 094            return;
 95
 1796        if (parameterTypes.Count == 0)
 97        {
 98            // No parameters at all - factory is pointless
 299            var diagnostic = Diagnostic.Create(
 2100                DiagnosticDescriptors.FactoryNoInjectableParams,
 2101                attributeSyntax.GetLocation(),
 2102                classSymbol.Name);
 103
 2104            context.ReportDiagnostic(diagnostic);
 2105            return;
 106        }
 107
 15108        int injectableCount = 0;
 15109        int runtimeCount = 0;
 110
 92111        foreach (var parameterType in parameterTypes)
 112        {
 31113            if (IsInjectableParameterType(parameterType))
 114            {
 15115                injectableCount++;
 116            }
 117            else
 118            {
 16119                runtimeCount++;
 120            }
 121        }
 122
 123        // NDLRGEN003: All params are injectable - factory unnecessary
 15124        if (runtimeCount == 0)
 125        {
 4126            var diagnostic = Diagnostic.Create(
 4127                DiagnosticDescriptors.FactoryAllParamsInjectable,
 4128                attributeSyntax.GetLocation(),
 4129                classSymbol.Name);
 130
 4131            context.ReportDiagnostic(diagnostic);
 132        }
 133        // NDLRGEN004: No params are injectable - low value factory
 11134        else if (injectableCount == 0)
 135        {
 4136            var diagnostic = Diagnostic.Create(
 4137                DiagnosticDescriptors.FactoryNoInjectableParams,
 4138                attributeSyntax.GetLocation(),
 4139                classSymbol.Name);
 140
 4141            context.ReportDiagnostic(diagnostic);
 142        }
 11143    }
 144
 145    /// <summary>
 146    /// Returns the effective constructor parameter types for a type eligible for
 147    /// generated-constructor generation (<c>[GenerateConstructor]</c> or a positive
 148    /// field-level constructor guard trigger), derived from the same shared eligible-field
 149    /// model the source generator and type-registry discovery use, or
 150    /// <see langword="null"/> when the type is not eligible. Such a type's effective
 151    /// constructor is emitted by a sibling generator pass rather than authored in its own
 152    /// syntax tree, so runtime-vs-injectable classification must use this field-derived
 153    /// parameter list instead of the type's (implicit, parameterless) symbol-visible
 154    /// constructor.
 155    /// </summary>
 156    private static IReadOnlyList<ITypeSymbol>? TryGetGeneratedConstructorParameterTypes(INamedTypeSymbol classSymbol)
 157    {
 17158        if (!GeneratedConstructorEligibility.IsEligibleForGeneratedConstructor(classSymbol))
 12159            return null;
 160
 5161        return GeneratedConstructorEligibility.GetEligibleConstructorFields(classSymbol)
 10162            .Select(f => f.Type)
 5163            .ToList();
 164    }
 165
 166    /// <summary>
 167    /// Returns the parameter types of the best (public, most-parameters) explicitly
 168    /// authored instance constructor, or <see langword="null"/> when the type declares no
 169    /// public instance constructor at all.
 170    /// </summary>
 171    private static IReadOnlyList<ITypeSymbol>? GetBestExplicitConstructorParameterTypes(INamedTypeSymbol classSymbol)
 172    {
 12173        var publicCtors = classSymbol.InstanceConstructors
 12174            .Where(c => c.DeclaredAccessibility == Accessibility.Public && !c.IsStatic)
 0175            .OrderByDescending(c => c.Parameters.Length)
 12176            .ToList();
 177
 12178        if (publicCtors.Count == 0)
 0179            return null;
 180
 33181        return publicCtors[0].Parameters.Select(p => p.Type).ToList();
 182    }
 183
 184    private static bool IsGenerateFactoryAttribute(INamedTypeSymbol attributeSymbol)
 185    {
 186        // Handle both GenerateFactoryAttribute and GenerateFactoryAttribute<T>
 268187        var name = attributeSymbol.Name;
 268188        if (name != GenerateFactoryAttributeName)
 189        {
 190            // Check original definition for generic case
 251191            if (attributeSymbol.IsGenericType)
 192            {
 0193                name = attributeSymbol.OriginalDefinition.Name;
 0194                if (name != GenerateFactoryAttributeName)
 0195                    return false;
 196            }
 197            else
 198            {
 251199                return false;
 200            }
 201        }
 202
 17203        var ns = attributeSymbol.ContainingNamespace?.ToString();
 17204        return ns == GeneratorsNamespace;
 205    }
 206
 207    private static bool IsInjectableParameterType(ITypeSymbol typeSymbol)
 208    {
 209        // Value types (int, string, DateTime, Guid, etc.) are NOT injectable
 31210        if (typeSymbol.IsValueType)
 5211            return false;
 212
 213        // String is special - it's a reference type but not injectable
 26214        if (typeSymbol.SpecialType == SpecialType.System_String)
 11215            return false;
 216
 217        // Delegates are not injectable
 15218        if (typeSymbol.TypeKind == TypeKind.Delegate)
 0219            return false;
 220
 221        // Arrays are typically not injectable by DI
 15222        if (typeSymbol.TypeKind == TypeKind.Array)
 0223            return false;
 224
 225        // Interfaces and classes are injectable
 15226        if (typeSymbol.TypeKind == TypeKind.Interface || typeSymbol.TypeKind == TypeKind.Class)
 15227            return true;
 228
 0229        return false;
 230    }
 231}