< Summary

Information
Class: NexusLabs.Needlr.Analyzers.DeferToContainerInGeneratedCodeAnalyzer
Assembly: NexusLabs.Needlr.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Analyzers/DeferToContainerInGeneratedCodeAnalyzer.cs
Line coverage
92%
Covered lines: 53
Uncovered lines: 4
Coverable lines: 57
Total lines: 173
Line coverage: 92.9%
Branch coverage
80%
Covered branches: 40
Total branches: 50
Branch coverage: 80%
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%
AnalyzeClassDeclaration(...)100%44100%
FindDeferToContainerAttribute(...)100%88100%
GetAttributeName(...)75%4483.33%
IsInGeneratedCode(...)94.44%181894.11%
HasGeneratedCodeAttribute(...)50%181680%

File(s)

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

#LineLine coverage
 1using System.Collections.Immutable;
 2using System.IO;
 3using System.Linq;
 4
 5using Microsoft.CodeAnalysis;
 6using Microsoft.CodeAnalysis.CSharp;
 7using Microsoft.CodeAnalysis.CSharp.Syntax;
 8using Microsoft.CodeAnalysis.Diagnostics;
 9
 10namespace NexusLabs.Needlr.Analyzers;
 11
 12/// <summary>
 13/// Analyzer that detects [DeferToContainer] attributes placed in generated code.
 14/// </summary>
 15/// <remarks>
 16/// <para>
 17/// Source generators run in isolation and cannot see output from other generators.
 18/// If another generator adds [DeferToContainer] to a partial class, Needlr's
 19/// TypeRegistryGenerator will not see it and will generate incorrect factory code.
 20/// </para>
 21/// <para>
 22/// This analyzer runs after all generators complete and can detect this scenario,
 23/// warning users to move the attribute to their original source file.
 24/// </para>
 25/// </remarks>
 26[DiagnosticAnalyzer(LanguageNames.CSharp)]
 27public sealed class DeferToContainerInGeneratedCodeAnalyzer : DiagnosticAnalyzer
 28{
 29    private const string DeferToContainerAttributeName = "DeferToContainerAttribute";
 30    private const string DeferToContainerAttributeShortName = "DeferToContainer";
 31    private const string DeferToContainerAttributeFullName = "NexusLabs.Needlr.DeferToContainerAttribute";
 32
 33    /// <inheritdoc />
 34    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 31435        ImmutableArray.Create(DiagnosticDescriptors.DeferToContainerInGeneratedCode);
 36
 37    /// <inheritdoc />
 38    public override void Initialize(AnalysisContext context)
 39    {
 40        // Important: We WANT to analyze generated code - that's the whole point!
 2641        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDia
 2642        context.EnableConcurrentExecution();
 43
 2644        context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration);
 2645    }
 46
 47    private static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context)
 48    {
 3249        var classDeclaration = (ClassDeclarationSyntax)context.Node;
 50
 51        // Check if this class has [DeferToContainer] attribute
 3252        var deferToContainerAttribute = FindDeferToContainerAttribute(classDeclaration);
 3253        if (deferToContainerAttribute == null)
 54        {
 1855            return;
 56        }
 57
 58        // Check if the attribute is in generated code
 1459        if (!IsInGeneratedCode(deferToContainerAttribute, context))
 60        {
 261            return;
 62        }
 63
 64        // Report diagnostic on the attribute
 1265        var diagnostic = Diagnostic.Create(
 1266            DiagnosticDescriptors.DeferToContainerInGeneratedCode,
 1267            deferToContainerAttribute.GetLocation(),
 1268            classDeclaration.Identifier.Text);
 69
 1270        context.ReportDiagnostic(diagnostic);
 1271    }
 72
 73    private static AttributeSyntax? FindDeferToContainerAttribute(ClassDeclarationSyntax classDeclaration)
 74    {
 11275        foreach (var attributeList in classDeclaration.AttributeLists)
 76        {
 11077            foreach (var attribute in attributeList.Attributes)
 78            {
 3179                var name = GetAttributeName(attribute);
 3180                if (name == DeferToContainerAttributeName ||
 3181                    name == DeferToContainerAttributeShortName)
 82                {
 1483                    return attribute;
 84                }
 85            }
 86        }
 87
 1888        return null;
 89    }
 90
 91    private static string? GetAttributeName(AttributeSyntax attribute)
 92    {
 3193        return attribute.Name switch
 3194        {
 1195            IdentifierNameSyntax identifier => identifier.Identifier.Text,
 2096            QualifiedNameSyntax qualified => qualified.Right.Identifier.Text,
 097            _ => null
 3198        };
 99    }
 100
 101    private static bool IsInGeneratedCode(SyntaxNode node, SyntaxNodeAnalysisContext context)
 102    {
 14103        var syntaxTree = node.SyntaxTree;
 104
 105        // Check file path for common generated code patterns
 14106        var filePath = syntaxTree.FilePath;
 14107        if (!string.IsNullOrEmpty(filePath))
 108        {
 109            // Common generated file patterns
 14110            if (filePath.EndsWith(".g.cs") ||
 14111                filePath.EndsWith(".generated.cs") ||
 14112                filePath.EndsWith(".designer.cs"))
 113            {
 10114                return true;
 115            }
 116
 117            // Check for obj/generated folder (common for source generators)
 4118            var normalizedPath = filePath.Replace('\\', '/').ToLowerInvariant();
 4119            if (normalizedPath.Contains("/obj/") && normalizedPath.Contains("/generated/"))
 120            {
 2121                return true;
 122            }
 123        }
 124
 125        // Check for [GeneratedCode] attribute on the class
 2126        var classDeclaration = node.FirstAncestorOrSelf<ClassDeclarationSyntax>();
 2127        if (classDeclaration != null)
 128        {
 2129            var symbol = context.SemanticModel.GetDeclaredSymbol(classDeclaration);
 2130            if (symbol != null && HasGeneratedCodeAttribute(symbol))
 131            {
 0132                return true;
 133            }
 134        }
 135
 136        // Check if the syntax tree is marked as generated
 2137        if (context.SemanticModel.Compilation.Options.SyntaxTreeOptionsProvider != null)
 138        {
 139            // The tree options provider can indicate generated code status
 140            // but we've already covered the main cases above
 141        }
 142
 2143        return false;
 144    }
 145
 146    private static bool HasGeneratedCodeAttribute(ISymbol symbol)
 147    {
 8148        foreach (var attribute in symbol.GetAttributes())
 149        {
 2150            var attrClass = attribute.AttributeClass;
 2151            if (attrClass == null)
 152            {
 153                continue;
 154            }
 155
 156            // Check for System.CodeDom.Compiler.GeneratedCodeAttribute
 2157            if (attrClass.Name == "GeneratedCodeAttribute" &&
 2158                attrClass.ContainingNamespace?.ToDisplayString() == "System.CodeDom.Compiler")
 159            {
 0160                return true;
 161            }
 162
 163            // Also check for CompilerGeneratedAttribute
 2164            if (attrClass.Name == "CompilerGeneratedAttribute" &&
 2165                attrClass.ContainingNamespace?.ToDisplayString() == "System.Runtime.CompilerServices")
 166            {
 0167                return true;
 168            }
 169        }
 170
 2171        return false;
 172    }
 173}