< Summary

Information
Class: NexusLabs.Needlr.Generators.HttpClientOptionsAttributeHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/HttpClientOptionsAttributeHelper.cs
Line coverage
5%
Covered lines: 6
Uncovered lines: 98
Coverable lines: 104
Total lines: 333
Line coverage: 5.7%
Branch coverage
4%
Covered branches: 5
Total branches: 108
Branch coverage: 4.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1using Microsoft.CodeAnalysis;
 2using Microsoft.CodeAnalysis.CSharp;
 3using Microsoft.CodeAnalysis.CSharp.Syntax;
 4
 5using NexusLabs.Needlr.Generators.Models;
 6
 7namespace NexusLabs.Needlr.Generators;
 8
 9/// <summary>
 10/// Helper for discovering <c>[HttpClientOptions]</c> attributes and the associated
 11/// capability interfaces on Roslyn symbols. Also resolves the HttpClient name from the
 12/// three supported sources (attribute argument, <c>ClientName</c> property, type-name
 13/// inference).
 14/// </summary>
 15internal static class HttpClientOptionsAttributeHelper
 16{
 17    private const string HttpClientOptionsAttributeName = "HttpClientOptionsAttribute";
 18    private const string GeneratorsNamespace = "NexusLabs.Needlr.Generators";
 19
 20    private const string INamedHttpClientOptionsName = "INamedHttpClientOptions";
 21    private const string IHttpClientTimeoutName = "IHttpClientTimeout";
 22    private const string IHttpClientUserAgentName = "IHttpClientUserAgent";
 23    private const string IHttpClientBaseAddressName = "IHttpClientBaseAddress";
 24    private const string IHttpClientDefaultHeadersName = "IHttpClientDefaultHeaders";
 25
 26    /// <summary>Suffixes stripped from the type name (in order) when inferring the client name.</summary>
 027    private static readonly string[] ClientNameSuffixes = ["HttpClientOptions", "HttpClientSettings", "HttpClient"];
 28
 29    /// <summary>
 30    /// Extracted state from an <c>[HttpClientOptions]</c> attribute on a type.
 31    /// </summary>
 32    public readonly struct HttpClientOptionsAttributeInfo
 33    {
 34        public HttpClientOptionsAttributeInfo(string? sectionName, string? name, Location? attributeLocation)
 35        {
 036            SectionName = sectionName;
 037            Name = name;
 038            AttributeLocation = attributeLocation;
 039        }
 40
 41        /// <summary>Explicit section name from the attribute, or null to infer.</summary>
 042        public string? SectionName { get; }
 43
 44        /// <summary>Explicit client name override from the attribute, or null to fall through.</summary>
 045        public string? Name { get; }
 46
 47        /// <summary>Source location of the attribute, used for analyzer diagnostics.</summary>
 048        public Location? AttributeLocation { get; }
 49    }
 50
 51    /// <summary>
 52    /// Checks if a type has the <c>[HttpClientOptions]</c> attribute.
 53    /// </summary>
 54    public static bool HasHttpClientOptionsAttribute(INamedTypeSymbol typeSymbol)
 55    {
 700277256        foreach (var attribute in typeSymbol.GetAttributes())
 57        {
 204412958            if (IsHttpClientOptionsAttribute(attribute.AttributeClass))
 059                return true;
 60        }
 61
 145725762        return false;
 63    }
 64
 65    /// <summary>
 66    /// Extracts the <c>[HttpClientOptions]</c> attribute info from a type, or <c>null</c>
 67    /// if the attribute is not present.
 68    /// </summary>
 69    public static HttpClientOptionsAttributeInfo? GetHttpClientOptionsAttribute(INamedTypeSymbol typeSymbol)
 70    {
 071        foreach (var attribute in typeSymbol.GetAttributes())
 72        {
 073            if (!IsHttpClientOptionsAttribute(attribute.AttributeClass))
 74                continue;
 75
 076            string? sectionName = null;
 077            if (attribute.ConstructorArguments.Length > 0 &&
 078                attribute.ConstructorArguments[0].Value is string section)
 79            {
 080                sectionName = section;
 81            }
 82
 083            string? name = null;
 084            foreach (var namedArg in attribute.NamedArguments)
 85            {
 086                if (namedArg.Key == "Name" && namedArg.Value.Value is string n)
 87                {
 088                    name = n;
 89                }
 90            }
 91
 092            var location = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation();
 093            return new HttpClientOptionsAttributeInfo(sectionName, name, location);
 94        }
 95
 096        return null;
 97    }
 98
 99    /// <summary>
 100    /// Detects which v1 capability interfaces the type implements. Returns a bit flag set
 101    /// which drives the conditional emission in <c>HttpClientCodeGenerator</c>.
 102    /// </summary>
 103    public static HttpClientCapabilities DetectCapabilities(INamedTypeSymbol typeSymbol)
 104    {
 0105        var caps = HttpClientCapabilities.None;
 106
 0107        foreach (var iface in typeSymbol.AllInterfaces)
 108        {
 0109            if (iface.ContainingNamespace?.ToDisplayString() != GeneratorsNamespace)
 110                continue;
 111
 0112            switch (iface.Name)
 113            {
 114                case IHttpClientTimeoutName:
 0115                    caps |= HttpClientCapabilities.Timeout;
 0116                    break;
 117                case IHttpClientUserAgentName:
 0118                    caps |= HttpClientCapabilities.UserAgent;
 0119                    break;
 120                case IHttpClientBaseAddressName:
 0121                    caps |= HttpClientCapabilities.BaseAddress;
 0122                    break;
 123                case IHttpClientDefaultHeadersName:
 0124                    caps |= HttpClientCapabilities.Headers;
 125                    break;
 126            }
 127        }
 128
 0129        return caps;
 130    }
 131
 132    /// <summary>
 133    /// Returns <c>true</c> if the type implements <c>INamedHttpClientOptions</c>.
 134    /// </summary>
 135    public static bool ImplementsNamedHttpClientOptions(INamedTypeSymbol typeSymbol)
 136    {
 0137        foreach (var iface in typeSymbol.AllInterfaces)
 138        {
 0139            if (iface.Name == INamedHttpClientOptionsName &&
 0140                iface.ContainingNamespace?.ToDisplayString() == GeneratorsNamespace)
 141            {
 0142                return true;
 143            }
 144        }
 145
 0146        return false;
 147    }
 148
 149    /// <summary>
 150    /// Resolves the HttpClient name from the three allowed sources, in precedence order:
 151    /// (1) attribute <c>Name</c>, (2) <c>ClientName</c> property literal body,
 152    /// (3) inferred from type name with suffix stripping.
 153    /// </summary>
 154    /// <param name="typeSymbol">The options type.</param>
 155    /// <param name="attributeInfo">The extracted attribute info for the type.</param>
 156    /// <param name="propertyNameFromType">
 157    /// The literal <c>ClientName</c> property value if present and resolvable, or <c>null</c>.
 158    /// </param>
 159    /// <param name="resolvedName">The resolved client name on success.</param>
 160    /// <returns><c>true</c> if a non-empty name could be resolved; otherwise <c>false</c>.</returns>
 161    public static bool TryResolveClientName(
 162        INamedTypeSymbol typeSymbol,
 163        HttpClientOptionsAttributeInfo attributeInfo,
 164        string? propertyNameFromType,
 165        out string resolvedName)
 166    {
 0167        if (!string.IsNullOrWhiteSpace(attributeInfo.Name))
 168        {
 0169            resolvedName = attributeInfo.Name!;
 0170            return true;
 171        }
 172
 0173        if (!string.IsNullOrWhiteSpace(propertyNameFromType))
 174        {
 0175            resolvedName = propertyNameFromType!;
 0176            return true;
 177        }
 178
 0179        var inferred = InferClientNameFromTypeName(typeSymbol.Name);
 0180        if (!string.IsNullOrWhiteSpace(inferred))
 181        {
 0182            resolvedName = inferred;
 0183            return true;
 184        }
 185
 0186        resolvedName = string.Empty;
 0187        return false;
 188    }
 189
 190    /// <summary>
 191    /// Strips known suffixes from a type name to infer a client name.
 192    /// Returns the original name if no suffix matches.
 193    /// </summary>
 194    public static string InferClientNameFromTypeName(string typeName)
 195    {
 0196        foreach (var suffix in ClientNameSuffixes)
 197        {
 0198            if (typeName.EndsWith(suffix, System.StringComparison.Ordinal) &&
 0199                typeName.Length > suffix.Length)
 200            {
 0201                return typeName.Substring(0, typeName.Length - suffix.Length);
 202            }
 203        }
 204
 0205        return typeName;
 206    }
 207
 208    /// <summary>
 209    /// Attempts to read a <c>ClientName</c> property from the type and extract its literal
 210    /// expression value. Returns a tri-state:
 211    /// <list type="bullet">
 212    /// <item><description><c>Absent</c> — no <c>ClientName</c> property declared</description></item>
 213    /// <item><description><c>Literal</c> — <c>ClientName</c> exists with a string literal expression body; value is in 
 214    /// <item><description><c>NonLiteral</c> — <c>ClientName</c> exists but its body is not a simple literal (e.g., comp
 215    /// </list>
 216    /// </summary>
 217    public static ClientNamePropertyResult TryGetClientNameProperty(
 218        INamedTypeSymbol typeSymbol,
 219        out string? literalValue)
 220    {
 0221        literalValue = null;
 0222        IPropertySymbol? clientNameProp = null;
 223
 0224        foreach (var member in typeSymbol.GetMembers("ClientName"))
 225        {
 0226            if (member is IPropertySymbol p)
 227            {
 0228                clientNameProp = p;
 0229                break;
 230            }
 231        }
 232
 0233        if (clientNameProp is null)
 0234            return ClientNamePropertyResult.Absent;
 235
 236        // Must be a string-typed, readable, instance property — otherwise treat as non-literal
 237        // and let the analyzer handle it (NDLRHTTP006 fires on shape violations).
 0238        if (clientNameProp.Type.SpecialType != SpecialType.System_String || clientNameProp.IsStatic)
 0239            return ClientNamePropertyResult.NonLiteral;
 240
 0241        foreach (var declRef in clientNameProp.DeclaringSyntaxReferences)
 242        {
 0243            var syntax = declRef.GetSyntax();
 0244            if (syntax is not PropertyDeclarationSyntax propSyntax)
 245                continue;
 246
 247            // Expression-bodied arrow form: public string ClientName => "WebFetch";
 0248            if (propSyntax.ExpressionBody is ArrowExpressionClauseSyntax arrow &&
 0249                arrow.Expression is LiteralExpressionSyntax exprLit &&
 0250                exprLit.IsKind(SyntaxKind.StringLiteralExpression))
 251            {
 0252                literalValue = exprLit.Token.ValueText;
 0253                return ClientNamePropertyResult.Literal;
 254            }
 255
 256            // Getter-only body form: public string ClientName { get { return "WebFetch"; } }
 0257            if (propSyntax.AccessorList is { } accessors)
 258            {
 0259                foreach (var accessor in accessors.Accessors)
 260                {
 0261                    if (!accessor.IsKind(SyntaxKind.GetAccessorDeclaration))
 262                        continue;
 263
 264                    // Expression body on getter
 0265                    if (accessor.ExpressionBody is ArrowExpressionClauseSyntax getArrow &&
 0266                        getArrow.Expression is LiteralExpressionSyntax getExprLit &&
 0267                        getExprLit.IsKind(SyntaxKind.StringLiteralExpression))
 268                    {
 0269                        literalValue = getExprLit.Token.ValueText;
 0270                        return ClientNamePropertyResult.Literal;
 271                    }
 272
 273                    // Single-return block body
 0274                    if (accessor.Body is { } block &&
 0275                        block.Statements.Count == 1 &&
 0276                        block.Statements[0] is ReturnStatementSyntax ret &&
 0277                        ret.Expression is LiteralExpressionSyntax retLit &&
 0278                        retLit.IsKind(SyntaxKind.StringLiteralExpression))
 279                    {
 0280                        literalValue = retLit.Token.ValueText;
 0281                        return ClientNamePropertyResult.Literal;
 282                    }
 283                }
 284            }
 285        }
 286
 0287        return ClientNamePropertyResult.NonLiteral;
 288    }
 289
 290    /// <summary>
 291    /// Computes the configuration section name from the attribute (if explicit) or by inference
 292    /// from the resolved client name.
 293    /// </summary>
 294    public static string ResolveSectionName(HttpClientOptionsAttributeInfo attributeInfo, string clientName)
 295    {
 0296        if (!string.IsNullOrWhiteSpace(attributeInfo.SectionName))
 0297            return attributeInfo.SectionName!;
 298
 0299        return $"HttpClients:{clientName}";
 300    }
 301
 302    /// <summary>
 303    /// Returns the symbol for the <c>ClientName</c> property if present, for analyzer shape checks.
 304    /// </summary>
 305    public static IPropertySymbol? GetClientNamePropertySymbol(INamedTypeSymbol typeSymbol)
 306    {
 0307        foreach (var member in typeSymbol.GetMembers("ClientName"))
 308        {
 0309            if (member is IPropertySymbol p)
 0310                return p;
 311        }
 0312        return null;
 313    }
 314
 315    private static bool IsHttpClientOptionsAttribute(INamedTypeSymbol? attributeClass)
 316    {
 2044129317        if (attributeClass is null)
 0318            return false;
 319
 2044129320        return attributeClass.Name == HttpClientOptionsAttributeName &&
 2044129321               attributeClass.ContainingNamespace?.ToDisplayString() == GeneratorsNamespace;
 322    }
 323}
 324
 325/// <summary>
 326/// Tri-state result from probing a type for a <c>ClientName</c> property.
 327/// </summary>
 328internal enum ClientNamePropertyResult
 329{
 330    Absent,
 331    Literal,
 332    NonLiteral,
 333}