< Summary

Information
Class: NexusLabs.Needlr.Generators.RegisterClosedOverImplementationsOfAttributeAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/RegisterClosedOverImplementationsOfAttributeAnalyzer.cs
Line coverage
79%
Covered lines: 69
Uncovered lines: 18
Coverable lines: 87
Total lines: 180
Line coverage: 79.3%
Branch coverage
65%
Covered branches: 42
Total branches: 64
Branch coverage: 65.6%
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(...)73.07%282686.84%
IsComposedAttribute(...)75%44100%
GetSourceTypeArgument(...)50%9871.42%
GetAsServiceType(...)87.5%8887.5%
IsOpenGenericInterface(...)66.66%171266.66%
ImplementsServiceType(...)16.66%17633.33%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/RegisterClosedOverImplementationsOfAttributeAnalyzer.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 [RegisterClosedOverImplementationsOf] attribute usage:
 13/// - NDLRGEN035: Source type argument must be an open generic interface
 14/// - NDLRGEN036: Composition class must be an open generic with matching arity
 15/// - NDLRGEN037: Composition class must specify and implement the As service type
 16/// </summary>
 17[DiagnosticAnalyzer(LanguageNames.CSharp)]
 18public sealed class RegisterClosedOverImplementationsOfAttributeAnalyzer : DiagnosticAnalyzer
 19{
 20    private const string AttributeName = "RegisterClosedOverImplementationsOfAttribute";
 21    private const string GeneratorsNamespace = "NexusLabs.Needlr.Generators";
 22
 23    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 30024        ImmutableArray.Create(
 30025            DiagnosticDescriptors.ComposedSourceNotOpenGenericInterface,
 30026            DiagnosticDescriptors.ComposedClassNotOpenGeneric,
 30027            DiagnosticDescriptors.ComposedClassNotImplementingAs);
 28
 29    public override void Initialize(AnalysisContext context)
 30    {
 2431        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2432        context.EnableConcurrentExecution();
 33
 2434        context.RegisterSyntaxNodeAction(AnalyzeAttribute, SyntaxKind.Attribute);
 2435    }
 36
 37    private static void AnalyzeAttribute(SyntaxNodeAnalysisContext context)
 38    {
 18039        var attributeSyntax = (AttributeSyntax)context.Node;
 18040        var attributeSymbol = context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol?.ContainingType;
 41
 18042        if (attributeSymbol is null)
 043            return;
 44
 18045        if (!IsComposedAttribute(attributeSymbol))
 16546            return;
 47
 1548        if (attributeSyntax.Parent?.Parent is not ClassDeclarationSyntax classDeclaration)
 049            return;
 50
 1551        if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol)
 052            return;
 53
 1554        var sourceType = GetSourceTypeArgument(attributeSyntax, context.SemanticModel);
 1555        if (sourceType is null)
 056            return;
 57
 58        // NDLRGEN035: source must be an open generic interface.
 1559        if (!IsOpenGenericInterface(sourceType, out var typeDescription))
 60        {
 461            context.ReportDiagnostic(Diagnostic.Create(
 462                DiagnosticDescriptors.ComposedSourceNotOpenGenericInterface,
 463                attributeSyntax.GetLocation(),
 464                sourceType.ToDisplayString(),
 465                typeDescription));
 466            return;
 67        }
 68
 1169        if (sourceType is not INamedTypeSymbol sourceInterface)
 070            return;
 71
 1172        var expectedArity = sourceInterface.TypeParameters.Length;
 73
 74        // NDLRGEN036: composition must be an open generic with matching arity.
 1175        if (!classSymbol.IsGenericType || classSymbol.TypeParameters.Length != expectedArity)
 76        {
 477            context.ReportDiagnostic(Diagnostic.Create(
 478                DiagnosticDescriptors.ComposedClassNotOpenGeneric,
 479                attributeSyntax.GetLocation(),
 480                classSymbol.Name,
 481                sourceInterface.ToDisplayString(),
 482                expectedArity));
 483            return;
 84        }
 85
 86        // NDLRGEN037: composition must specify and implement the As service type.
 787        var asType = GetAsServiceType(attributeSyntax, context.SemanticModel);
 788        if (asType is null || !ImplementsServiceType(classSymbol, asType))
 89        {
 490            context.ReportDiagnostic(Diagnostic.Create(
 491                DiagnosticDescriptors.ComposedClassNotImplementingAs,
 492                attributeSyntax.GetLocation(),
 493                classSymbol.Name));
 94        }
 795    }
 96
 97    private static bool IsComposedAttribute(INamedTypeSymbol attributeSymbol) =>
 18098        attributeSymbol.Name == AttributeName &&
 18099        attributeSymbol.ContainingNamespace?.ToString() == GeneratorsNamespace;
 100
 101    private static ITypeSymbol? GetSourceTypeArgument(AttributeSyntax attributeSyntax, SemanticModel semanticModel)
 102    {
 15103        var argumentList = attributeSyntax.ArgumentList;
 15104        if (argumentList is null || argumentList.Arguments.Count == 0)
 0105            return null;
 106
 107        // The first positional argument is the source open generic interface.
 30108        var firstArgument = argumentList.Arguments.FirstOrDefault(a => a.NameEquals is null);
 15109        if (firstArgument?.Expression is TypeOfExpressionSyntax typeOfExpression)
 15110            return semanticModel.GetTypeInfo(typeOfExpression.Type).Type;
 111
 0112        return null;
 113    }
 114
 115    private static ITypeSymbol? GetAsServiceType(AttributeSyntax attributeSyntax, SemanticModel semanticModel)
 116    {
 7117        var argumentList = attributeSyntax.ArgumentList;
 7118        if (argumentList is null)
 0119            return null;
 120
 7121        var asArgument = argumentList.Arguments
 19122            .FirstOrDefault(a => a.NameEquals?.Name.Identifier.Text == "As");
 123
 7124        if (asArgument?.Expression is TypeOfExpressionSyntax typeOfExpression)
 5125            return semanticModel.GetTypeInfo(typeOfExpression.Type).Type;
 126
 2127        return null;
 128    }
 129
 130    private static bool IsOpenGenericInterface(ITypeSymbol? typeSymbol, out string typeDescription)
 131    {
 15132        if (typeSymbol is null)
 133        {
 0134            typeDescription = "null";
 0135            return false;
 136        }
 137
 15138        if (typeSymbol is not INamedTypeSymbol namedType)
 139        {
 0140            typeDescription = $"{typeSymbol.TypeKind}";
 0141            return false;
 142        }
 143
 15144        if (namedType.TypeKind != TypeKind.Interface)
 145        {
 2146            typeDescription = $"{namedType.TypeKind} (not an interface)";
 2147            return false;
 148        }
 149
 13150        if (!namedType.IsGenericType)
 151        {
 2152            typeDescription = "non-generic interface";
 2153            return false;
 154        }
 155
 11156        if (!namedType.IsUnboundGenericType &&
 11157            !namedType.TypeArguments.All(t => t.TypeKind == TypeKind.TypeParameter))
 158        {
 0159            typeDescription = "closed generic interface (use typeof(IInterface<>) not typeof(IInterface<T>))";
 0160            return false;
 161        }
 162
 11163        typeDescription = "open generic interface";
 11164        return true;
 165    }
 166
 167    private static bool ImplementsServiceType(INamedTypeSymbol classSymbol, ITypeSymbol serviceType)
 168    {
 5169        if (serviceType.TypeKind == TypeKind.Interface)
 10170            return classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, serviceType));
 171
 0172        for (var baseType = classSymbol.BaseType; baseType is not null; baseType = baseType.BaseType)
 173        {
 0174            if (SymbolEqualityComparer.Default.Equals(baseType, serviceType))
 0175                return true;
 176        }
 177
 0178        return false;
 179    }
 180}