< Summary

Information
Class: NexusLabs.Needlr.Logging.Analyzers.NeedlrLoggerMessageAnalyzer
Assembly: NexusLabs.Needlr.Logging.Analyzers
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Logging.Analyzers/NeedlrLoggerMessageAnalyzer.cs
Line coverage
90%
Covered lines: 84
Uncovered lines: 9
Coverable lines: 93
Total lines: 230
Line coverage: 90.3%
Branch coverage
80%
Covered branches: 63
Total branches: 78
Branch coverage: 80.7%
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(...)50%2294.44%
AnalyzeMethod(...)84.61%262696.29%
HasAttribute(...)75%4475%
IsContainingTypePartial(...)87.5%8887.5%
HasAccessibleLogger(...)77.77%211880%
CountMessageParameters(...)78.57%151483.33%
IsLogger(...)50%2266.66%
IsException(...)100%44100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Logging.Analyzers/NeedlrLoggerMessageAnalyzer.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.Logging.Analyzers;
 10
 11/// <summary>
 12/// Validates usage of <c>[NeedlrLoggerMessage]</c> so misconfigurations surface as build diagnostics
 13/// rather than confusing generated-code errors.
 14/// </summary>
 15[DiagnosticAnalyzer(LanguageNames.CSharp)]
 16public sealed class NeedlrLoggerMessageAnalyzer : DiagnosticAnalyzer
 17{
 18    private const string AttributeFullName = "NexusLabs.Needlr.Logging.NeedlrLoggerMessageAttribute";
 19    private const string ILoggerFullName = "Microsoft.Extensions.Logging.ILogger";
 20    private const string ExceptionFullName = "System.Exception";
 21    private const int MaxDefineParameters = 6;
 22
 23    /// <inheritdoc />
 24    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 33325        ImmutableArray.Create(
 33326            DiagnosticDescriptors.MustBePartial,
 33327            DiagnosticDescriptors.MustReturnVoid,
 33328            DiagnosticDescriptors.MustNotBeGeneric,
 33329            DiagnosticDescriptors.ContainingTypeMustBePartial,
 33330            DiagnosticDescriptors.LoggerNotFound,
 33331            DiagnosticDescriptors.TooManyParameters);
 32
 33    /// <inheritdoc />
 34    public override void Initialize(AnalysisContext context)
 35    {
 2036        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 2037        context.EnableConcurrentExecution();
 38
 2039        context.RegisterCompilationStartAction(static compilationStart =>
 2040        {
 1341            var attributeSymbol = compilationStart.Compilation.GetTypeByMetadataName(AttributeFullName);
 1342            if (attributeSymbol is null)
 2043            {
 044                return;
 2045            }
 2046
 1347            var loggerSymbol = compilationStart.Compilation.GetTypeByMetadataName(ILoggerFullName);
 1348            var exceptionSymbol = compilationStart.Compilation.GetTypeByMetadataName(ExceptionFullName);
 2049
 1350            compilationStart.RegisterSymbolAction(
 1551                symbolContext => AnalyzeMethod(symbolContext, attributeSymbol, loggerSymbol, exceptionSymbol),
 1352                SymbolKind.Method);
 3353        });
 2054    }
 55
 56    private static void AnalyzeMethod(
 57        SymbolAnalysisContext context,
 58        INamedTypeSymbol attributeSymbol,
 59        INamedTypeSymbol? loggerSymbol,
 60        INamedTypeSymbol? exceptionSymbol)
 61    {
 1562        var method = (IMethodSymbol)context.Symbol;
 1563        if (!HasAttribute(method, attributeSymbol))
 64        {
 065            return;
 66        }
 67
 68        // For a partial method with both parts, analyze only the definition to avoid double-reporting.
 1569        if (method.PartialDefinitionPart is not null)
 70        {
 271            return;
 72        }
 73
 1374        var location = method.Locations.Length > 0 ? method.Locations[0] : Location.None;
 75
 1376        if (!method.IsPartialDefinition && method.PartialDefinitionPart is null)
 77        {
 478            context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MustBePartial, location, method.Name));
 79        }
 80
 1381        if (!method.ReturnsVoid)
 82        {
 283            context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MustReturnVoid, location, method.Name));
 84        }
 85
 1386        if (method.IsGenericMethod)
 87        {
 288            context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.MustNotBeGeneric, location, method.Name));
 89        }
 90
 1391        if (!IsContainingTypePartial(method))
 92        {
 293            context.ReportDiagnostic(Diagnostic.Create(
 294                DiagnosticDescriptors.ContainingTypeMustBePartial,
 295                location,
 296                method.ContainingType?.Name ?? method.Name));
 97        }
 98
 1399        if (loggerSymbol is not null && !HasAccessibleLogger(method, loggerSymbol))
 100        {
 2101            context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.LoggerNotFound, location, method.Name));
 102        }
 103
 13104        var messageParameterCount = CountMessageParameters(method, loggerSymbol, exceptionSymbol);
 13105        if (messageParameterCount > MaxDefineParameters)
 106        {
 2107            context.ReportDiagnostic(Diagnostic.Create(
 2108                DiagnosticDescriptors.TooManyParameters,
 2109                location,
 2110                method.Name,
 2111                messageParameterCount));
 112        }
 13113    }
 114
 115    private static bool HasAttribute(IMethodSymbol method, INamedTypeSymbol attributeSymbol)
 116    {
 45117        foreach (var attribute in method.GetAttributes())
 118        {
 15119            if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeSymbol))
 120            {
 15121                return true;
 122            }
 123        }
 124
 0125        return false;
 126    }
 127
 128    private static bool IsContainingTypePartial(IMethodSymbol method)
 129    {
 13130        var containingType = method.ContainingType;
 13131        if (containingType is null)
 132        {
 0133            return true;
 134        }
 135
 41136        foreach (var reference in containingType.DeclaringSyntaxReferences)
 137        {
 13138            if (reference.GetSyntax() is TypeDeclarationSyntax typeDeclaration &&
 13139                typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
 140            {
 11141                return true;
 142            }
 143        }
 144
 2145        return false;
 146    }
 147
 148    private static bool HasAccessibleLogger(IMethodSymbol method, INamedTypeSymbol loggerSymbol)
 149    {
 13150        if (method.IsStatic)
 151        {
 0152            return method.Parameters.Any(parameter => IsLogger(parameter.Type, loggerSymbol));
 153        }
 154
 34155        for (var type = method.ContainingType; type is not null; type = type.BaseType)
 156        {
 85157            foreach (var member in type.GetMembers())
 158            {
 33159                if (member.IsStatic)
 160                {
 161                    continue;
 162                }
 163
 29164                if (member is IFieldSymbol field && IsLogger(field.Type, loggerSymbol))
 165                {
 11166                    return true;
 167                }
 168
 18169                if (member is IPropertySymbol { GetMethod: not null } property && IsLogger(property.Type, loggerSymbol))
 170                {
 0171                    return true;
 172                }
 173            }
 174        }
 175
 2176        return false;
 177    }
 178
 179    private static int CountMessageParameters(
 180        IMethodSymbol method,
 181        INamedTypeSymbol? loggerSymbol,
 182        INamedTypeSymbol? exceptionSymbol)
 183    {
 13184        var count = 0;
 13185        var loggerConsumed = !method.IsStatic;
 13186        var exceptionConsumed = false;
 187
 82188        foreach (var parameter in method.Parameters)
 189        {
 28190            if (!loggerConsumed && loggerSymbol is not null && IsLogger(parameter.Type, loggerSymbol))
 191            {
 0192                loggerConsumed = true;
 0193                continue;
 194            }
 195
 28196            if (!exceptionConsumed && exceptionSymbol is not null && IsException(parameter.Type, exceptionSymbol))
 197            {
 9198                exceptionConsumed = true;
 9199                continue;
 200            }
 201
 19202            count++;
 203        }
 204
 13205        return count;
 206    }
 207
 208    private static bool IsLogger(ITypeSymbol type, INamedTypeSymbol loggerSymbol)
 209    {
 11210        if (SymbolEqualityComparer.Default.Equals(type, loggerSymbol))
 211        {
 11212            return true;
 213        }
 214
 0215        return type.AllInterfaces.Any(iface => SymbolEqualityComparer.Default.Equals(iface, loggerSymbol));
 216    }
 217
 218    private static bool IsException(ITypeSymbol type, INamedTypeSymbol exceptionSymbol)
 219    {
 162220        for (var current = type; current is not null; current = current.BaseType)
 221        {
 62222            if (SymbolEqualityComparer.Default.Equals(current, exceptionSymbol))
 223            {
 9224                return true;
 225            }
 226        }
 227
 19228        return false;
 229    }
 230}