< Summary

Information
Class: NexusLabs.Needlr.Generators.OpenDecoratorForAttributeAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/OpenDecoratorForAttributeAnalyzer.cs
Line coverage
85%
Covered lines: 79
Uncovered lines: 13
Coverable lines: 92
Total lines: 201
Line coverage: 85.8%
Branch coverage
70%
Covered branches: 38
Total branches: 54
Branch coverage: 70.3%
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(...)70.83%252488.63%
IsOpenDecoratorForAttribute(...)75%44100%
GetTypeArgumentFromAttribute(...)50%6677.77%
IsOpenGenericInterface(...)66.66%171266.66%
ImplementsOpenGenericInterface(...)87.5%88100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/OpenDecoratorForAttributeAnalyzer.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 [OpenDecoratorFor] attribute usage:
 13/// - NDLRGEN006: Type argument must be an open generic interface
 14/// - NDLRGEN007: Decorator class must be an open generic with matching arity
 15/// - NDLRGEN008: Decorator class must implement the open generic interface
 16/// </summary>
 17[DiagnosticAnalyzer(LanguageNames.CSharp)]
 18public sealed class OpenDecoratorForAttributeAnalyzer : DiagnosticAnalyzer
 19{
 20    private const string OpenDecoratorForAttributeName = "OpenDecoratorForAttribute";
 21    private const string GeneratorsNamespace = "NexusLabs.Needlr.Generators";
 22
 23    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 25624        ImmutableArray.Create(
 25625            DiagnosticDescriptors.OpenDecoratorTypeNotOpenGeneric,
 25626            DiagnosticDescriptors.OpenDecoratorClassNotOpenGeneric,
 25627            DiagnosticDescriptors.OpenDecoratorNotImplementingInterface);
 28
 29    public override void Initialize(AnalysisContext context)
 30    {
 2331        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2332        context.EnableConcurrentExecution();
 33
 2334        context.RegisterSyntaxNodeAction(AnalyzeAttribute, SyntaxKind.Attribute);
 2335    }
 36
 37    private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context)
 38    {
 16939        var attributeSyntax = (AttributeSyntax)context.Node;
 16940        var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol?.ContainingType;
 41
 16942        if (attributeSymbol == null)
 043            return;
 44
 45        // Check if this is an [OpenDecoratorFor] attribute
 16946        if (!IsOpenDecoratorForAttribute(attributeSymbol))
 15447            return;
 48
 49        // Get the class this attribute is applied to
 1550        var classDeclaration = attributeSyntax.Parent?.Parent as ClassDeclarationSyntax;
 1551        if (classDeclaration == null)
 052            return;
 53
 1554        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 1555        if (classSymbol == null)
 056            return;
 57
 58        // Get the type argument from the attribute constructor
 1559        var typeArg = GetTypeArgumentFromAttribute(attributeSyntax, context.SemanticModel);
 1560        if (typeArg == null)
 061            return;
 62
 63        // NDLRGEN006: Validate type argument is an open generic interface
 1564        if (!IsOpenGenericInterface(typeArg, out string typeDescription))
 65        {
 466            var diagnostic = Diagnostic.Create(
 467                DiagnosticDescriptors.OpenDecoratorTypeNotOpenGeneric,
 468                attributeSyntax.GetLocation(),
 469                typeArg.ToDisplayString(),
 470                typeDescription);
 71
 472            context.ReportDiagnostic(diagnostic);
 473            return; // Don't check other rules if this fails
 74        }
 75
 1176        var openGenericInterface = typeArg as INamedTypeSymbol;
 1177        if (openGenericInterface == null)
 078            return;
 79
 1180        int expectedTypeParamCount = openGenericInterface.TypeParameters.Length;
 81
 82        // NDLRGEN007: Validate decorator class is an open generic with matching arity
 1183        if (!classSymbol.IsGenericType || classSymbol.TypeParameters.Length != expectedTypeParamCount)
 84        {
 485            var diagnostic = Diagnostic.Create(
 486                DiagnosticDescriptors.OpenDecoratorClassNotOpenGeneric,
 487                attributeSyntax.GetLocation(),
 488                classSymbol.Name,
 489                openGenericInterface.ToDisplayString(),
 490                expectedTypeParamCount);
 91
 492            context.ReportDiagnostic(diagnostic);
 493            return; // Don't check implementation if arity doesn't match
 94        }
 95
 96        // NDLRGEN008: Validate decorator implements the open generic interface
 797        if (!ImplementsOpenGenericInterface(classSymbol, openGenericInterface))
 98        {
 299            var diagnostic = Diagnostic.Create(
 2100                DiagnosticDescriptors.OpenDecoratorNotImplementingInterface,
 2101                attributeSyntax.GetLocation(),
 2102                classSymbol.Name,
 2103                openGenericInterface.ToDisplayString());
 104
 2105            context.ReportDiagnostic(diagnostic);
 106        }
 7107    }
 108
 109    private static bool IsOpenDecoratorForAttribute(INamedTypeSymbol attributeSymbol)
 110    {
 169111        if (attributeSymbol.Name != OpenDecoratorForAttributeName)
 154112            return false;
 113
 15114        var ns = attributeSymbol.ContainingNamespace?.ToString();
 15115        return ns == GeneratorsNamespace;
 116    }
 117
 118    private static ITypeSymbol? GetTypeArgumentFromAttribute(
 119        AttributeSyntax attributeSyntax,
 120        SemanticModel semanticModel)
 121    {
 122        // The attribute has a constructor: OpenDecoratorForAttribute(Type openGenericServiceType)
 123        // We need to get the typeof() argument
 15124        var argumentList = attributeSyntax.ArgumentList;
 15125        if (argumentList == null || argumentList.Arguments.Count == 0)
 0126            return null;
 127
 15128        var firstArg = argumentList.Arguments[0];
 15129        var expression = firstArg.Expression;
 130
 131        // Handle typeof(IHandler<>)
 15132        if (expression is TypeOfExpressionSyntax typeOfExpr)
 133        {
 15134            var typeInfo = semanticModel.GetTypeInfo(typeOfExpr.Type);
 15135            return typeInfo.Type;
 136        }
 137
 0138        return null;
 139    }
 140
 141    private static bool IsOpenGenericInterface(ITypeSymbol? typeSymbol, out string typeDescription)
 142    {
 15143        if (typeSymbol == null)
 144        {
 0145            typeDescription = "null";
 0146            return false;
 147        }
 148
 15149        if (typeSymbol is not INamedTypeSymbol namedType)
 150        {
 0151            typeDescription = $"{typeSymbol.TypeKind}";
 0152            return false;
 153        }
 154
 15155        if (namedType.TypeKind != TypeKind.Interface)
 156        {
 2157            typeDescription = $"{namedType.TypeKind} (not an interface)";
 2158            return false;
 159        }
 160
 13161        if (!namedType.IsGenericType)
 162        {
 2163            typeDescription = "non-generic interface";
 2164            return false;
 165        }
 166
 167        // Check if it's an unbound/open generic (has unsubstituted type parameters)
 11168        if (!namedType.IsUnboundGenericType &&
 11169            !namedType.TypeArguments.All(t => t.TypeKind == TypeKind.TypeParameter))
 170        {
 0171            typeDescription = "closed generic interface (use typeof(IInterface<>) not typeof(IInterface<T>))";
 0172            return false;
 173        }
 174
 11175        typeDescription = "open generic interface";
 11176        return true;
 177    }
 178
 179    private static bool ImplementsOpenGenericInterface(
 180        INamedTypeSymbol classSymbol,
 181        INamedTypeSymbol openGenericInterface)
 182    {
 183        // Check if the class implements a constructed version of the open generic interface
 184        // e.g., LoggingDecorator<T> : IHandler<T> should match IHandler<>
 7185        var unboundInterface = openGenericInterface.IsUnboundGenericType
 7186            ? openGenericInterface
 7187            : openGenericInterface.ConstructUnboundGenericType();
 188
 23189        foreach (var iface in classSymbol.AllInterfaces)
 190        {
 7191            if (!iface.IsGenericType)
 192                continue;
 193
 7194            var unboundImplemented = iface.ConstructUnboundGenericType();
 7195            if (SymbolEqualityComparer.Default.Equals(unboundImplemented, unboundInterface))
 5196                return true;
 197        }
 198
 2199        return false;
 200    }
 201}