< Summary

Information
Class: NexusLabs.Needlr.Generators.HttpClientOptionsAnalyzer
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/HttpClientOptionsAnalyzer.cs
Line coverage
100%
Covered lines: 123
Uncovered lines: 0
Coverable lines: 123
Total lines: 200
Line coverage: 100%
Branch coverage
88%
Covered branches: 48
Total branches: 54
Branch coverage: 88.8%
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(...)80%1010100%
AnalyzeNamedType(...)90.9%4444100%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/HttpClientOptionsAnalyzer.cs

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System.Collections.Concurrent;
 5using System.Collections.Generic;
 6using System.Collections.Immutable;
 7using System.Linq;
 8
 9using Microsoft.CodeAnalysis;
 10using Microsoft.CodeAnalysis.CSharp;
 11using Microsoft.CodeAnalysis.CSharp.Syntax;
 12using Microsoft.CodeAnalysis.Diagnostics;
 13
 14namespace NexusLabs.Needlr.Generators;
 15
 16/// <summary>
 17/// Analyzer for <c>[HttpClientOptions]</c> usage. Enforces the contracts the generator
 18/// relies on and reports six diagnostics:
 19/// <list type="bullet">
 20/// <item><description>NDLRHTTP001 — target must implement <c>INamedHttpClientOptions</c></description></item>
 21/// <item><description>NDLRHTTP002 — attribute <c>Name</c> and <c>ClientName</c> property disagree</description></item>
 22/// <item><description>NDLRHTTP003 — <c>ClientName</c> property body is not a literal expression</description></item>
 23/// <item><description>NDLRHTTP004 — resolved name is empty</description></item>
 24/// <item><description>NDLRHTTP005 — duplicate client name across types in the compilation</description></item>
 25/// <item><description>NDLRHTTP006 — <c>ClientName</c> property has the wrong shape</description></item>
 26/// </list>
 27/// </summary>
 28[DiagnosticAnalyzer(LanguageNames.CSharp)]
 29public sealed class HttpClientOptionsAnalyzer : DiagnosticAnalyzer
 30{
 31    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 40032        ImmutableArray.Create(
 40033            DiagnosticDescriptors.HttpClientMustImplementMarker,
 40034            DiagnosticDescriptors.HttpClientNameSourceConflict,
 40035            DiagnosticDescriptors.HttpClientNamePropertyNotLiteral,
 40036            DiagnosticDescriptors.HttpClientNameEmpty,
 40037            DiagnosticDescriptors.HttpClientNameCollision,
 40038            DiagnosticDescriptors.HttpClientNamePropertyWrongShape);
 39
 40    public override void Initialize(AnalysisContext context)
 41    {
 3242        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 3243        context.EnableConcurrentExecution();
 44
 45        // Collision detection needs the full compilation — use a compilation start action so
 46        // we can collect per-type resolved names from concurrent symbol actions and then
 47        // emit collision diagnostics in a compilation-end action.
 3248        context.RegisterCompilationStartAction(compilationContext =>
 3249        {
 2050            var nameToTypes = new ConcurrentDictionary<string, List<(INamedTypeSymbol Type, Location Location)>>();
 3251
 2052            compilationContext.RegisterSymbolAction(
 6453                symbolContext => AnalyzeNamedType(symbolContext, nameToTypes),
 2054                SymbolKind.NamedType);
 3255
 2056            compilationContext.RegisterCompilationEndAction(endContext =>
 2057            {
 7258                foreach (var kvp in nameToTypes)
 2059                {
 1660                    var participants = kvp.Value
 1661                        .OrderBy(
 1662                            participant =>
 463                                participant.Location.SourceTree?.FilePath ??
 464                                string.Empty,
 1665                            System.StringComparer.Ordinal)
 1666                        .ThenBy(
 1667                            participant =>
 468                                participant.Location.SourceSpan.Start)
 1669                        .ThenBy(
 1670                            participant =>
 471                                participant.Type.ToDisplayString(),
 1672                            System.StringComparer.Ordinal)
 1673                        .ToArray();
 1674                    if (participants.Length < 2)
 2075                        continue;
 2076
 2077                    // Report the collision on every participant after the first, pointing at
 2078                    // the prior participant so both ends of the clash surface in the IDE.
 279                    var first = participants[0];
 880                    for (var i = 1; i < participants.Length; i++)
 2081                    {
 282                        var dup = participants[i];
 283                        endContext.ReportDiagnostic(Diagnostic.Create(
 284                            DiagnosticDescriptors.HttpClientNameCollision,
 285                            dup.Location,
 286                            dup.Type.Name,
 287                            kvp.Key,
 288                            first.Type.Name));
 2089                    }
 2090                }
 4091            });
 5292        });
 3293    }
 94
 95    private static void AnalyzeNamedType(
 96        SymbolAnalysisContext context,
 97        ConcurrentDictionary<string, List<(INamedTypeSymbol Type, Location Location)>> nameToTypes)
 98    {
 6499        var typeSymbol = (INamedTypeSymbol)context.Symbol;
 64100        var attrInfo = HttpClientOptionsAttributeHelper.GetHttpClientOptionsAttribute(typeSymbol);
 64101        if (!attrInfo.HasValue)
 40102            return;
 103
 24104        var typeLocation = typeSymbol.Locations.Length > 0 ? typeSymbol.Locations[0] : Location.None;
 24105        var reportLocation = attrInfo.Value.AttributeLocation ?? typeLocation;
 106
 107        // NDLRHTTP001: must implement INamedHttpClientOptions
 24108        if (!HttpClientOptionsAttributeHelper.ImplementsNamedHttpClientOptions(typeSymbol))
 109        {
 2110            context.ReportDiagnostic(Diagnostic.Create(
 2111                DiagnosticDescriptors.HttpClientMustImplementMarker,
 2112                reportLocation,
 2113                typeSymbol.Name));
 114            // Keep analyzing — the other diagnostics are still meaningful.
 115        }
 116
 117        // NDLRHTTP006: ClientName property shape check — runs before the literal extraction so
 118        // a wrong-shape property doesn't silently fall through to type-name inference.
 24119        var clientNameSymbol = HttpClientOptionsAttributeHelper.GetClientNamePropertySymbol(typeSymbol);
 24120        if (clientNameSymbol is not null)
 121        {
 10122            var isValidShape =
 10123                clientNameSymbol.Type.SpecialType == SpecialType.System_String &&
 10124                !clientNameSymbol.IsStatic &&
 10125                clientNameSymbol.GetMethod is not null;
 126
 10127            if (!isValidShape)
 128            {
 2129                context.ReportDiagnostic(Diagnostic.Create(
 2130                    DiagnosticDescriptors.HttpClientNamePropertyWrongShape,
 2131                    clientNameSymbol.Locations.Length > 0 ? clientNameSymbol.Locations[0] : reportLocation,
 2132                    typeSymbol.Name));
 133            }
 134        }
 135
 136        // Now resolve the name and check conflict / literal / empty rules.
 24137        var propResult = HttpClientOptionsAttributeHelper.TryGetClientNameProperty(typeSymbol, out var literalValue);
 24138        var attributeName = attrInfo.Value.Name;
 139
 140        // NDLRHTTP003: non-literal ClientName without an attribute Name fallback
 24141        if (propResult == ClientNamePropertyResult.NonLiteral && string.IsNullOrWhiteSpace(attributeName))
 142        {
 143            // Only report if the property shape was otherwise valid — NDLRHTTP006 already fired
 144            // for shape issues, and piling NDLRHTTP003 on top would be noise.
 4145            if (clientNameSymbol is not null &&
 4146                clientNameSymbol.Type.SpecialType == SpecialType.System_String &&
 4147                !clientNameSymbol.IsStatic &&
 4148                clientNameSymbol.GetMethod is not null)
 149            {
 2150                context.ReportDiagnostic(Diagnostic.Create(
 2151                    DiagnosticDescriptors.HttpClientNamePropertyNotLiteral,
 2152                    clientNameSymbol.Locations.Length > 0 ? clientNameSymbol.Locations[0] : reportLocation,
 2153                    typeSymbol.Name));
 154            }
 155        }
 156
 157        // NDLRHTTP002: attribute Name and literal ClientName property disagree
 24158        if (!string.IsNullOrWhiteSpace(attributeName) &&
 24159            propResult == ClientNamePropertyResult.Literal &&
 24160            !string.IsNullOrWhiteSpace(literalValue) &&
 24161            !string.Equals(attributeName, literalValue, System.StringComparison.Ordinal))
 162        {
 2163            context.ReportDiagnostic(Diagnostic.Create(
 2164                DiagnosticDescriptors.HttpClientNameSourceConflict,
 2165                reportLocation,
 2166                typeSymbol.Name,
 2167                attributeName!,
 2168                literalValue!));
 169        }
 170
 24171        var propertyName = propResult == ClientNamePropertyResult.Literal
 24172            ? literalValue
 24173            : null;
 24174        if (!HttpClientOptionsAttributeHelper.TryResolveClientName(
 24175            typeSymbol,
 24176            attrInfo.Value,
 24177            propertyName,
 24178            out var effectiveName))
 179        {
 6180            context.ReportDiagnostic(Diagnostic.Create(
 6181                DiagnosticDescriptors.HttpClientNameEmpty,
 6182                reportLocation,
 6183                typeSymbol.Name));
 6184            return;
 185        }
 186
 187        // Record for NDLRHTTP005 collision detection.
 18188        nameToTypes.AddOrUpdate(
 18189            effectiveName,
 16190            _ => new List<(INamedTypeSymbol, Location)> { (typeSymbol, reportLocation) },
 18191            (_, existing) =>
 18192            {
 2193                lock (existing)
 18194                {
 2195                    existing.Add((typeSymbol, reportLocation));
 2196                    return existing;
 18197                }
 20198            });
 18199    }
 200}