< 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
0%
Covered lines: 0
Uncovered lines: 109
Coverable lines: 109
Total lines: 191
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 58
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_SupportedDiagnostics()100%210%
Initialize(...)0%4260%
AnalyzeNamedType(...)0%2756520%

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;
 7
 8using Microsoft.CodeAnalysis;
 9using Microsoft.CodeAnalysis.CSharp;
 10using Microsoft.CodeAnalysis.CSharp.Syntax;
 11using Microsoft.CodeAnalysis.Diagnostics;
 12
 13namespace NexusLabs.Needlr.Generators;
 14
 15/// <summary>
 16/// Analyzer for <c>[HttpClientOptions]</c> usage. Enforces the contracts the generator
 17/// relies on and reports six diagnostics:
 18/// <list type="bullet">
 19/// <item><description>NDLRHTTP001 — target must implement <c>INamedHttpClientOptions</c></description></item>
 20/// <item><description>NDLRHTTP002 — attribute <c>Name</c> and <c>ClientName</c> property disagree</description></item>
 21/// <item><description>NDLRHTTP003 — <c>ClientName</c> property body is not a literal expression</description></item>
 22/// <item><description>NDLRHTTP004 — resolved name is empty</description></item>
 23/// <item><description>NDLRHTTP005 — duplicate client name across types in the compilation</description></item>
 24/// <item><description>NDLRHTTP006 — <c>ClientName</c> property has the wrong shape</description></item>
 25/// </list>
 26/// </summary>
 27[DiagnosticAnalyzer(LanguageNames.CSharp)]
 28public sealed class HttpClientOptionsAnalyzer : DiagnosticAnalyzer
 29{
 30    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
 031        ImmutableArray.Create(
 032            DiagnosticDescriptors.HttpClientMustImplementMarker,
 033            DiagnosticDescriptors.HttpClientNameSourceConflict,
 034            DiagnosticDescriptors.HttpClientNamePropertyNotLiteral,
 035            DiagnosticDescriptors.HttpClientNameEmpty,
 036            DiagnosticDescriptors.HttpClientNameCollision,
 037            DiagnosticDescriptors.HttpClientNamePropertyWrongShape);
 38
 39    public override void Initialize(AnalysisContext context)
 40    {
 041        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 042        context.EnableConcurrentExecution();
 43
 44        // Collision detection needs the full compilation — use a compilation start action so
 45        // we can collect per-type resolved names from concurrent symbol actions and then
 46        // emit collision diagnostics in a compilation-end action.
 047        context.RegisterCompilationStartAction(compilationContext =>
 048        {
 049            var nameToTypes = new ConcurrentDictionary<string, List<(INamedTypeSymbol Type, Location Location)>>();
 050
 051            compilationContext.RegisterSymbolAction(
 052                symbolContext => AnalyzeNamedType(symbolContext, nameToTypes),
 053                SymbolKind.NamedType);
 054
 055            compilationContext.RegisterCompilationEndAction(endContext =>
 056            {
 057                foreach (var kvp in nameToTypes)
 058                {
 059                    if (kvp.Value.Count < 2)
 060                        continue;
 061
 062                    // Report the collision on every participant after the first, pointing at
 063                    // the prior participant so both ends of the clash surface in the IDE.
 064                    var first = kvp.Value[0];
 065                    for (int i = 1; i < kvp.Value.Count; i++)
 066                    {
 067                        var dup = kvp.Value[i];
 068                        endContext.ReportDiagnostic(Diagnostic.Create(
 069                            DiagnosticDescriptors.HttpClientNameCollision,
 070                            dup.Location,
 071                            dup.Type.Name,
 072                            kvp.Key,
 073                            first.Type.Name));
 074                    }
 075                }
 076            });
 077        });
 078    }
 79
 80    private static void AnalyzeNamedType(
 81        SymbolAnalysisContext context,
 82        ConcurrentDictionary<string, List<(INamedTypeSymbol Type, Location Location)>> nameToTypes)
 83    {
 084        if (context.Symbol is not INamedTypeSymbol typeSymbol)
 085            return;
 86
 087        if (!HttpClientOptionsAttributeHelper.HasHttpClientOptionsAttribute(typeSymbol))
 088            return;
 89
 090        var attrInfo = HttpClientOptionsAttributeHelper.GetHttpClientOptionsAttribute(typeSymbol);
 091        if (!attrInfo.HasValue)
 092            return;
 93
 094        var typeLocation = typeSymbol.Locations.Length > 0 ? typeSymbol.Locations[0] : Location.None;
 095        var reportLocation = attrInfo.Value.AttributeLocation ?? typeLocation;
 96
 97        // NDLRHTTP001: must implement INamedHttpClientOptions
 098        if (!HttpClientOptionsAttributeHelper.ImplementsNamedHttpClientOptions(typeSymbol))
 99        {
 0100            context.ReportDiagnostic(Diagnostic.Create(
 0101                DiagnosticDescriptors.HttpClientMustImplementMarker,
 0102                reportLocation,
 0103                typeSymbol.Name));
 104            // Keep analyzing — the other diagnostics are still meaningful.
 105        }
 106
 107        // NDLRHTTP006: ClientName property shape check — runs before the literal extraction so
 108        // a wrong-shape property doesn't silently fall through to type-name inference.
 0109        var clientNameSymbol = HttpClientOptionsAttributeHelper.GetClientNamePropertySymbol(typeSymbol);
 0110        if (clientNameSymbol is not null)
 111        {
 0112            var isValidShape =
 0113                clientNameSymbol.Type.SpecialType == SpecialType.System_String &&
 0114                !clientNameSymbol.IsStatic &&
 0115                clientNameSymbol.GetMethod is not null;
 116
 0117            if (!isValidShape)
 118            {
 0119                context.ReportDiagnostic(Diagnostic.Create(
 0120                    DiagnosticDescriptors.HttpClientNamePropertyWrongShape,
 0121                    clientNameSymbol.Locations.Length > 0 ? clientNameSymbol.Locations[0] : reportLocation,
 0122                    typeSymbol.Name));
 123            }
 124        }
 125
 126        // Now resolve the name and check conflict / literal / empty rules.
 0127        var propResult = HttpClientOptionsAttributeHelper.TryGetClientNameProperty(typeSymbol, out var literalValue);
 0128        var attributeName = attrInfo.Value.Name;
 129
 130        // NDLRHTTP003: non-literal ClientName without an attribute Name fallback
 0131        if (propResult == ClientNamePropertyResult.NonLiteral && string.IsNullOrWhiteSpace(attributeName))
 132        {
 133            // Only report if the property shape was otherwise valid — NDLRHTTP006 already fired
 134            // for shape issues, and piling NDLRHTTP003 on top would be noise.
 0135            if (clientNameSymbol is not null &&
 0136                clientNameSymbol.Type.SpecialType == SpecialType.System_String &&
 0137                !clientNameSymbol.IsStatic &&
 0138                clientNameSymbol.GetMethod is not null)
 139            {
 0140                context.ReportDiagnostic(Diagnostic.Create(
 0141                    DiagnosticDescriptors.HttpClientNamePropertyNotLiteral,
 0142                    clientNameSymbol.Locations.Length > 0 ? clientNameSymbol.Locations[0] : reportLocation,
 0143                    typeSymbol.Name));
 144            }
 145        }
 146
 147        // NDLRHTTP002: attribute Name and literal ClientName property disagree
 0148        if (!string.IsNullOrWhiteSpace(attributeName) &&
 0149            propResult == ClientNamePropertyResult.Literal &&
 0150            !string.IsNullOrWhiteSpace(literalValue) &&
 0151            !string.Equals(attributeName, literalValue, System.StringComparison.Ordinal))
 152        {
 0153            context.ReportDiagnostic(Diagnostic.Create(
 0154                DiagnosticDescriptors.HttpClientNameSourceConflict,
 0155                reportLocation,
 0156                typeSymbol.Name,
 0157                attributeName!,
 0158                literalValue!));
 159        }
 160
 161        // Compute the effective name (attribute wins, then literal property, then inferred).
 162        // This matches HttpClientOptionsAttributeHelper.TryResolveClientName exactly.
 0163        var effectiveName =
 0164            !string.IsNullOrWhiteSpace(attributeName) ? attributeName :
 0165            (propResult == ClientNamePropertyResult.Literal && !string.IsNullOrWhiteSpace(literalValue)) ? literalValue 
 0166            HttpClientOptionsAttributeHelper.InferClientNameFromTypeName(typeSymbol.Name);
 167
 168        // NDLRHTTP004: empty resolved name
 0169        if (string.IsNullOrWhiteSpace(effectiveName))
 170        {
 0171            context.ReportDiagnostic(Diagnostic.Create(
 0172                DiagnosticDescriptors.HttpClientNameEmpty,
 0173                reportLocation,
 0174                typeSymbol.Name));
 0175            return;
 176        }
 177
 178        // Record for NDLRHTTP005 collision detection.
 0179        nameToTypes.AddOrUpdate(
 0180            effectiveName!,
 0181            _ => new List<(INamedTypeSymbol, Location)> { (typeSymbol, reportLocation) },
 0182            (_, existing) =>
 0183            {
 0184                lock (existing)
 0185                {
 0186                    existing.Add((typeSymbol, reportLocation));
 0187                    return existing;
 0188                }
 0189            });
 0190    }
 191}