< Summary

Information
Class: NexusLabs.Needlr.Analyzers.GlobalNamespaceTypeAnalyzer
Assembly: NexusLabs.Needlr.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/GlobalNamespaceTypeAnalyzer.cs
Line coverage
97%
Covered lines: 84
Uncovered lines: 2
Coverable lines: 86
Total lines: 157
Line coverage: 97.6%
Branch coverage
87%
Covered branches: 61
Total branches: 70
Branch coverage: 87.1%
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(...)94.44%1818100%
GetGenerateTypeRegistryInfo(...)92.85%1414100%
HasAttribute(...)83.33%66100%
IsLikelyInjectableType(...)81.25%333288.88%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/GlobalNamespaceTypeAnalyzer.cs

#LineLine coverage
 1using System.Collections.Immutable;
 2
 3using Microsoft.CodeAnalysis;
 4using Microsoft.CodeAnalysis.Diagnostics;
 5
 6namespace NexusLabs.Needlr.Analyzers;
 7
 8/// <summary>
 9/// Analyzer that detects injectable types in the global namespace that won't be
 10/// discovered when IncludeNamespacePrefixes is set without an empty string.
 11/// </summary>
 12[DiagnosticAnalyzer(LanguageNames.CSharp)]
 13public sealed class GlobalNamespaceTypeAnalyzer : DiagnosticAnalyzer
 14{
 15    private const string GenerateTypeRegistryAttributeName = "NexusLabs.Needlr.Generators.GenerateTypeRegistryAttribute"
 16    private const string SingletonAttributeName = "NexusLabs.Needlr.SingletonAttribute";
 17    private const string ScopedAttributeName = "NexusLabs.Needlr.ScopedAttribute";
 18    private const string TransientAttributeName = "NexusLabs.Needlr.TransientAttribute";
 19    private const string DoNotInjectAttributeName = "NexusLabs.Needlr.DoNotInjectAttribute";
 20    private const string DoNotAutoRegisterAttributeName = "NexusLabs.Needlr.DoNotAutoRegisterAttribute";
 21
 22    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 18723        ImmutableArray.Create(DiagnosticDescriptors.GlobalNamespaceTypeNotDiscovered);
 24
 25    public override void Initialize(AnalysisContext context)
 26    {
 2527        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2528        context.EnableConcurrentExecution();
 29
 2530        context.RegisterCompilationStartAction(compilationContext =>
 2531        {
 2532            // Check if the assembly has [GenerateTypeRegistry] with IncludeNamespacePrefixes set
 1433            var (hasAttribute, prefixes, includesEmptyString) = GetGenerateTypeRegistryInfo(compilationContext.Compilati
 2534
 1435            if (!hasAttribute)
 136                return;
 2537
 2538            // If no prefixes are set, all types are included (no warning needed)
 1339            if (prefixes == null || prefixes.Length == 0)
 140                return;
 2541
 2542            // If empty string is in prefixes, global namespace types are included
 1243            if (includesEmptyString)
 144                return;
 2545
 2546            // Register to check all named types
 1147            compilationContext.RegisterSymbolAction(symbolContext =>
 1148            {
 9249                var typeSymbol = (INamedTypeSymbol)symbolContext.Symbol;
 1150
 1151                // Only check types in the global namespace
 9252                if (typeSymbol.ContainingNamespace?.IsGlobalNamespace != true)
 8053                    return;
 1154
 1155                // Skip types that are excluded from injection
 1256                if (HasAttribute(typeSymbol, DoNotInjectAttributeName) ||
 1257                    HasAttribute(typeSymbol, DoNotAutoRegisterAttributeName))
 258                    return;
 1159
 1160                // Check if this type looks injectable
 1061                if (!IsLikelyInjectableType(typeSymbol))
 462                    return;
 1163
 1164                // Report diagnostic
 665                var diagnostic = Diagnostic.Create(
 666                    DiagnosticDescriptors.GlobalNamespaceTypeNotDiscovered,
 667                    typeSymbol.Locations.FirstOrDefault(),
 668                    typeSymbol.Name);
 1169
 670                symbolContext.ReportDiagnostic(diagnostic);
 1171
 1772            }, SymbolKind.NamedType);
 3673        });
 2574    }
 75
 76    private static (bool hasAttribute, string[]? prefixes, bool includesEmptyString) GetGenerateTypeRegistryInfo(IAssemb
 77    {
 4178        foreach (var attribute in assembly.GetAttributes())
 79        {
 1380            var attrClass = attribute.AttributeClass;
 1381            if (attrClass?.ToDisplayString() != GenerateTypeRegistryAttributeName)
 82                continue;
 83
 1384            string[]? prefixes = null;
 1385            var includesEmptyString = false;
 86
 5087            foreach (var namedArg in attribute.NamedArguments)
 88            {
 1289                if (namedArg.Key == "IncludeNamespacePrefixes" &&
 1290                    !namedArg.Value.IsNull &&
 1291                    namedArg.Value.Values.Length > 0)
 92                {
 1293                    prefixes = namedArg.Value.Values
 1394                        .Where(v => v.Value is string)
 1395                        .Select(v => (string)v.Value!)
 1296                        .ToArray();
 97
 2598                    includesEmptyString = prefixes.Any(p => string.IsNullOrEmpty(p));
 99                }
 100            }
 101
 13102            return (true, prefixes, includesEmptyString);
 103        }
 104
 1105        return (false, null, false);
 106    }
 107
 108    private static bool HasAttribute(INamedTypeSymbol typeSymbol, string attributeName)
 109    {
 88110        foreach (var attribute in typeSymbol.GetAttributes())
 111        {
 9112            if (attribute.AttributeClass?.ToDisplayString() == attributeName)
 4113                return true;
 114        }
 33115        return false;
 116    }
 117
 118    private static bool IsLikelyInjectableType(INamedTypeSymbol typeSymbol)
 119    {
 120        // Skip abstract types, interfaces, and static classes
 10121        if (typeSymbol.IsAbstract || typeSymbol.TypeKind == TypeKind.Interface || typeSymbol.IsStatic)
 4122            return false;
 123
 124        // Skip generic type definitions (open generics)
 6125        if (typeSymbol.IsGenericType && typeSymbol.TypeParameters.Length > 0 && typeSymbol.TypeArguments.Length == 0)
 0126            return false;
 127
 128        // Check if it has lifetime attributes
 6129        if (HasAttribute(typeSymbol, SingletonAttributeName) ||
 6130            HasAttribute(typeSymbol, ScopedAttributeName) ||
 6131            HasAttribute(typeSymbol, TransientAttributeName))
 2132            return true;
 133
 134        // Check if it implements any interfaces (common for DI services)
 4135        if (typeSymbol.AllInterfaces.Length > 0)
 2136            return true;
 137
 138        // Check if it has a constructor with interface/class parameters (likely DI dependencies)
 6139        foreach (var constructor in typeSymbol.Constructors)
 140        {
 2141            if (constructor.IsStatic)
 142                continue;
 143
 6144            foreach (var parameter in constructor.Parameters)
 145            {
 2146                var paramType = parameter.Type;
 2147                if (paramType.TypeKind == TypeKind.Interface ||
 2148                    (paramType.TypeKind == TypeKind.Class && paramType.SpecialType == SpecialType.None))
 149                {
 2150                    return true;
 151                }
 152            }
 153        }
 154
 0155        return false;
 156    }
 157}