< Summary

Information
Class: NexusLabs.Needlr.Generators.ProviderDiscoveryHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/ProviderDiscoveryHelper.cs
Line coverage
91%
Covered lines: 98
Uncovered lines: 9
Coverable lines: 107
Total lines: 297
Line coverage: 91.5%
Branch coverage
86%
Covered branches: 100
Total branches: 116
Branch coverage: 86.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
HasProviderAttribute(...)87.5%8890%
GetProviderAttribute(...)75%8887.5%
DiscoverProvider(...)62.5%8894.44%
IsPartialType(...)100%66100%
ExtractPropertiesFromInterface()100%1010100%
ExtractPropertiesFromAttribute()93.33%303092.85%
DerivePropertyName(...)100%1212100%
Pluralize(...)50%361657.14%
IsVowel(...)100%210%
DeriveFactoryTypeName(...)100%88100%
DeterminePropertyKind(...)100%1010100%

File(s)

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

#LineLine coverage
 1using System.Collections.Generic;
 2using System.Linq;
 3using Microsoft.CodeAnalysis;
 4using NexusLabs.Needlr.Generators.Models;
 5
 6namespace NexusLabs.Needlr.Generators;
 7
 8/// <summary>
 9/// Helper for discovering [Provider] attributes from Roslyn symbols.
 10/// </summary>
 11internal static class ProviderDiscoveryHelper
 12{
 13    private const string ProviderAttributeName = "ProviderAttribute";
 14    private const string ProviderAttributeFullName = "NexusLabs.Needlr.Generators.ProviderAttribute";
 15
 16    /// <summary>
 17    /// Checks if a type has the [Provider] attribute.
 18    /// </summary>
 19    public static bool HasProviderAttribute(INamedTypeSymbol typeSymbol)
 20    {
 910696621        foreach (var attribute in typeSymbol.GetAttributes())
 22        {
 273304523            var attributeClass = attribute.AttributeClass;
 273304524            if (attributeClass == null)
 25                continue;
 26
 273304527            var name = attributeClass.Name;
 273304528            if (name == ProviderAttributeName)
 2429                return true;
 30
 273302131            var fullName = attributeClass.ToDisplayString();
 273302132            if (fullName == ProviderAttributeFullName)
 033                return true;
 34        }
 35
 182042636        return false;
 37    }
 38
 39    /// <summary>
 40    /// Gets the [Provider] attribute data from a type.
 41    /// </summary>
 42    public static AttributeData? GetProviderAttribute(INamedTypeSymbol typeSymbol)
 43    {
 5444        foreach (var attribute in typeSymbol.GetAttributes())
 45        {
 1846            var attributeClass = attribute.AttributeClass;
 1847            if (attributeClass == null)
 48                continue;
 49
 1850            var name = attributeClass.Name;
 1851            var fullName = attributeClass.ToDisplayString();
 52
 1853            if (name == ProviderAttributeName || fullName == ProviderAttributeFullName)
 1854                return attribute;
 55        }
 56
 057        return null;
 58    }
 59
 60    /// <summary>
 61    /// Discovers a provider from a type symbol with [Provider] attribute.
 62    /// </summary>
 63    /// <param name="typeSymbol">The type symbol to discover.</param>
 64    /// <param name="assemblyName">The assembly name containing the type.</param>
 65    /// <param name="generatedNamespace">The namespace where generated types are placed (e.g., "AssemblyName.Generated")
 66    public static DiscoveredProvider? DiscoverProvider(INamedTypeSymbol typeSymbol, string assemblyName, string generate
 67    {
 1868        var providerAttr = GetProviderAttribute(typeSymbol);
 1869        if (providerAttr == null)
 070            return null;
 71
 1872        var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol);
 1873        var isInterface = typeSymbol.TypeKind == TypeKind.Interface;
 1874        var isPartial = IsPartialType(typeSymbol);
 1875        var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath;
 76
 1877        var properties = new List<ProviderPropertyInfo>();
 78
 1879        if (isInterface)
 80        {
 81            // Interface mode: Extract properties from interface definition
 1282            properties.AddRange(ExtractPropertiesFromInterface(typeSymbol));
 83        }
 84        else
 85        {
 86            // Class mode: Extract from attribute constructor args and named args
 687            properties.AddRange(ExtractPropertiesFromAttribute(providerAttr, generatedNamespace));
 88        }
 89
 1890        return new DiscoveredProvider(
 1891            typeName,
 1892            assemblyName,
 1893            isInterface,
 1894            isPartial,
 1895            properties,
 1896            sourceFilePath);
 97    }
 98
 99    /// <summary>
 100    /// Checks if a type is declared as partial.
 101    /// </summary>
 102    private static bool IsPartialType(INamedTypeSymbol typeSymbol)
 103    {
 66104        foreach (var syntaxRef in typeSymbol.DeclaringSyntaxReferences)
 105        {
 18106            var syntax = syntaxRef.GetSyntax();
 18107            if (syntax is Microsoft.CodeAnalysis.CSharp.Syntax.TypeDeclarationSyntax typeDecl)
 108            {
 42109                if (typeDecl.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword)))
 6110                    return true;
 111            }
 112        }
 12113        return false;
 114    }
 115
 116    /// <summary>
 117    /// Extracts provider properties from an interface's get-only properties.
 118    /// </summary>
 119    private static IEnumerable<ProviderPropertyInfo> ExtractPropertiesFromInterface(INamedTypeSymbol interfaceSymbol)
 120    {
 84121        foreach (var member in interfaceSymbol.GetMembers())
 122        {
 30123            if (member is IPropertySymbol property && property.GetMethod != null && property.SetMethod == null)
 124            {
 15125                var propertyName = property.Name;
 15126                if (property.Type is INamedTypeSymbol namedType)
 127                {
 15128                    var serviceTypeName = TypeDiscoveryHelper.GetFullyQualifiedName(namedType);
 15129                    var kind = DeterminePropertyKind(property.Type, property.NullableAnnotation);
 130
 15131                    yield return new ProviderPropertyInfo(propertyName, serviceTypeName, kind);
 132                }
 133            }
 134        }
 12135    }
 136
 137    /// <summary>
 138    /// Extracts provider properties from [Provider] attribute arguments.
 139    /// </summary>
 140    /// <param name="attribute">The attribute data to extract from.</param>
 141    /// <param name="generatedNamespace">The namespace where generated types are placed.</param>
 142    private static IEnumerable<ProviderPropertyInfo> ExtractPropertiesFromAttribute(AttributeData attribute, string gene
 143    {
 144        // Process constructor arguments (required services)
 6145        if (attribute.ConstructorArguments.Length > 0)
 146        {
 2147            var firstArg = attribute.ConstructorArguments[0];
 2148            if (firstArg.Kind == TypedConstantKind.Array)
 149            {
 10150                foreach (var typeArg in firstArg.Values)
 151                {
 3152                    if (typeArg.Value is INamedTypeSymbol typeSymbol)
 153                    {
 3154                        var propertyName = DerivePropertyName(typeSymbol);
 3155                        var serviceTypeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol);
 3156                        yield return new ProviderPropertyInfo(propertyName, serviceTypeName, ProviderPropertyKind.Requir
 157                    }
 158                }
 159            }
 160        }
 161
 162        // Process named arguments
 20163        foreach (var namedArg in attribute.NamedArguments)
 164        {
 4165            var kind = namedArg.Key switch
 4166            {
 0167                "Required" => ProviderPropertyKind.Required,
 2168                "Optional" => ProviderPropertyKind.Optional,
 1169                "Collections" => ProviderPropertyKind.Collection,
 1170                "Factories" => ProviderPropertyKind.Factory,
 0171                _ => (ProviderPropertyKind?)null
 4172            };
 173
 4174            if (kind.HasValue && namedArg.Value.Kind == TypedConstantKind.Array)
 175            {
 16176                foreach (var typeArg in namedArg.Value.Values)
 177                {
 4178                    if (typeArg.Value is INamedTypeSymbol typeSymbol)
 179                    {
 4180                        var propertyName = DerivePropertyName(typeSymbol, kind.Value);
 4181                        var serviceTypeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol);
 182
 183                        // For collections, wrap in IEnumerable<T>
 4184                        if (kind.Value == ProviderPropertyKind.Collection)
 185                        {
 1186                            serviceTypeName = $"global::System.Collections.Generic.IEnumerable<{serviceTypeName}>";
 187                        }
 188                        // For factories, convert to factory interface type
 3189                        else if (kind.Value == ProviderPropertyKind.Factory)
 190                        {
 1191                            serviceTypeName = DeriveFactoryTypeName(typeSymbol, generatedNamespace);
 192                        }
 193
 4194                        yield return new ProviderPropertyInfo(propertyName, serviceTypeName, kind.Value);
 195                    }
 196                }
 197            }
 198        }
 6199    }
 200
 201    /// <summary>
 202    /// Derives a property name from a type (e.g., IOrderRepository → OrderRepository).
 203    /// For collections, pluralizes the name (e.g., IHandler → Handlers).
 204    /// For factories, appends Factory suffix (e.g., IOrderService → OrderServiceFactory).
 205    /// </summary>
 206    private static string DerivePropertyName(INamedTypeSymbol typeSymbol, ProviderPropertyKind kind = ProviderPropertyKi
 207    {
 7208        var name = typeSymbol.Name;
 209
 210        // Remove leading 'I' from interface names
 7211        if (typeSymbol.TypeKind == TypeKind.Interface && name.StartsWith("I") && name.Length > 1 && char.IsUpper(name[1]
 212        {
 7213            name = name.Substring(1);
 214        }
 215
 216        // Pluralize collection property names
 7217        if (kind == ProviderPropertyKind.Collection)
 218        {
 1219            name = Pluralize(name);
 220        }
 221        // Append Factory suffix for factory properties
 6222        else if (kind == ProviderPropertyKind.Factory)
 223        {
 1224            name = name + "Factory";
 225        }
 226
 7227        return name;
 228    }
 229
 230    /// <summary>
 231    /// Simple pluralization for property names.
 232    /// </summary>
 233    private static string Pluralize(string name)
 234    {
 1235        if (string.IsNullOrEmpty(name))
 0236            return name;
 237
 238        // Basic pluralization rules
 1239        if (name.EndsWith("y") && name.Length > 1 && !IsVowel(name[name.Length - 2]))
 240        {
 0241            return name.Substring(0, name.Length - 1) + "ies";
 242        }
 1243        if (name.EndsWith("s") || name.EndsWith("x") || name.EndsWith("ch") || name.EndsWith("sh"))
 244        {
 0245            return name + "es";
 246        }
 1247        return name + "s";
 248    }
 249
 0250    private static bool IsVowel(char c) => "aeiouAEIOU".Contains(c);
 251
 252    /// <summary>
 253    /// Derives a factory interface type name from a service type.
 254    /// E.g., IOrderService → IOrderServiceFactory (in Generated namespace)
 255    /// </summary>
 256    /// <param name="typeSymbol">The type to derive the factory name from.</param>
 257    /// <param name="generatedNamespace">The namespace where generated types are placed.</param>
 258    private static string DeriveFactoryTypeName(INamedTypeSymbol typeSymbol, string generatedNamespace)
 259    {
 1260        var name = typeSymbol.Name;
 261
 262        // Remove leading 'I' from interface names to get base name
 1263        if (typeSymbol.TypeKind == TypeKind.Interface && name.StartsWith("I") && name.Length > 1 && char.IsUpper(name[1]
 264        {
 1265            name = name.Substring(1);
 266        }
 267
 268        // Factory interface is I{Name}Factory in the assembly's generated namespace
 1269        return $"global::{generatedNamespace}.I{name}Factory";
 270    }
 271
 272    /// <summary>
 273    /// Determines the property kind based on the type.
 274    /// </summary>
 275    private static ProviderPropertyKind DeterminePropertyKind(ITypeSymbol type, NullableAnnotation nullableAnnotation)
 276    {
 277        // Check for IEnumerable<T>
 15278        if (type is INamedTypeSymbol namedType)
 279        {
 15280            var displayName = namedType.OriginalDefinition.ToDisplayString();
 15281            if (displayName.StartsWith("System.Collections.Generic.IEnumerable<") ||
 15282                displayName.StartsWith("System.Collections.Generic.IReadOnlyCollection<") ||
 15283                displayName.StartsWith("System.Collections.Generic.IReadOnlyList<"))
 284            {
 2285                return ProviderPropertyKind.Collection;
 286            }
 287        }
 288
 289        // Check for nullable annotation
 13290        if (nullableAnnotation == NullableAnnotation.Annotated)
 291        {
 2292            return ProviderPropertyKind.Optional;
 293        }
 294
 11295        return ProviderPropertyKind.Required;
 296    }
 297}