< Summary

Information
Class: NexusLabs.Needlr.Generators.AssemblyDiscoveryHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/AssemblyDiscoveryHelper.cs
Line coverage
42%
Covered lines: 59
Uncovered lines: 79
Coverable lines: 138
Total lines: 289
Line coverage: 42.7%
Branch coverage
50%
Covered branches: 49
Total branches: 98
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
DiscoverReferencedAssembliesWithTypeRegistry(...)100%88100%
DiscoverReferencedAssemblyTypesForDiagnostics(...)97.05%3434100%
DiscoverReferencedAssemblyTypesForGraph(...)19.04%13134210.34%
GetInterfaceLocationsFromServiceCatalog(...)0%210140%

File(s)

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

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Linq;
 7
 8using Microsoft.CodeAnalysis;
 9
 10using NexusLabs.Needlr.Generators.Models;
 11
 12namespace NexusLabs.Needlr.Generators;
 13
 14/// <summary>
 15/// Discovers types and assemblies from referenced projects that have
 16/// the <c>[GenerateTypeRegistry]</c> attribute for diagnostics, graph export,
 17/// and force-loading.
 18/// </summary>
 19internal static class AssemblyDiscoveryHelper
 20{
 21    /// <summary>
 22    /// Discovers all referenced assemblies that have the [GenerateTypeRegistry] attribute.
 23    /// These assemblies need to be force-loaded to ensure their module initializers run.
 24    /// </summary>
 25    internal static IReadOnlyList<string> DiscoverReferencedAssembliesWithTypeRegistry(Compilation compilation)
 26    {
 45827        var result = new List<string>();
 28
 15562629        foreach (var reference in compilation.References)
 30        {
 7735531            if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol)
 32            {
 33                // Skip the current assembly
 7716834                if (SymbolEqualityComparer.Default.Equals(assemblySymbol, compilation.Assembly))
 35                    continue;
 36
 7716837                if (TypeDiscoveryHelper.HasGenerateTypeRegistryAttribute(assemblySymbol))
 38                {
 1839                    result.Add(assemblySymbol.Name);
 40                }
 41            }
 42        }
 43
 45844        return result;
 45    }
 46
 47    /// <summary>
 48    /// Discovers types from referenced assemblies with [GenerateTypeRegistry] for diagnostics purposes.
 49    /// Unlike the main discovery, this includes internal types since we're just showing them in diagnostics.
 50    /// </summary>
 51    internal static Dictionary<string, List<DiagnosticTypeInfo>> DiscoverReferencedAssemblyTypesForDiagnostics(Compilati
 52    {
 9553        var result = new Dictionary<string, List<DiagnosticTypeInfo>>();
 54
 3232855        foreach (var reference in compilation.References)
 56        {
 1606957            if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol)
 58            {
 59                // Skip the current assembly
 1606960                if (SymbolEqualityComparer.Default.Equals(assemblySymbol, compilation.Assembly))
 61                    continue;
 62
 1606963                if (!TypeDiscoveryHelper.HasGenerateTypeRegistryAttribute(assemblySymbol))
 64                    continue;
 65
 1466                var assemblyTypes = new List<DiagnosticTypeInfo>();
 67
 68                // First pass: collect intercepted service names so we can identify their proxies
 1469                var interceptedServiceNames = new HashSet<string>();
 12070                foreach (var typeSymbol in TypeDiscoveryHelper.GetAllTypes(assemblySymbol.GlobalNamespace))
 71                {
 4672                    if (InterceptorDiscoveryHelper.HasInterceptAttributes(typeSymbol))
 73                    {
 274                        interceptedServiceNames.Add(typeSymbol.Name);
 75                    }
 76                }
 77
 12078                foreach (var typeSymbol in TypeDiscoveryHelper.GetAllTypes(assemblySymbol.GlobalNamespace))
 79                {
 80                    // Check if it's a registerable type (injectable, plugin, factory source, or interceptor)
 4681                    var hasFactoryAttr = FactoryDiscoveryHelper.HasGenerateFactoryAttribute(typeSymbol);
 4682                    var hasInterceptAttr = InterceptorDiscoveryHelper.HasInterceptAttributes(typeSymbol);
 4683                    var isInterceptorProxy = typeSymbol.Name.EndsWith("_InterceptorProxy");
 84
 4685                    if (!hasFactoryAttr && !hasInterceptAttr && !isInterceptorProxy &&
 4686                        !TypeDiscoveryHelper.WouldBeInjectableIgnoringAccessibility(typeSymbol) &&
 4687                        !TypeDiscoveryHelper.WouldBePluginIgnoringAccessibility(typeSymbol, compilation.Assembly))
 88                        continue;
 89
 1890                    var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol);
 1891                    var shortName = typeSymbol.Name;
 1892                    var lifetime = TypeDiscoveryHelper.DetermineLifetime(typeSymbol) ?? GeneratorLifetime.Singleton;
 1893                    var interfaces = TypeDiscoveryHelper.GetRegisterableInterfaces(typeSymbol, compilation.Assembly)
 1494                        .Select(i => TypeDiscoveryHelper.GetFullyQualifiedName(i))
 1895                        .ToArray();
 1896                    var dependencies = TypeDiscoveryHelper.GetBestConstructorParameters(typeSymbol)?
 1897                        .ToArray() ?? Array.Empty<string>();
 1898                    var isDecorator = TypeDiscoveryHelper.HasDecoratorForAttribute(typeSymbol) ||
 1899                                      OpenDecoratorDiscoveryHelper.HasOpenDecoratorForAttribute(typeSymbol);
 18100                    var isPlugin = TypeDiscoveryHelper.WouldBePluginIgnoringAccessibility(typeSymbol, compilation.Assemb
 18101                    var keyedValues = TypeDiscoveryHelper.GetKeyedServiceKeys(typeSymbol);
 18102                    var keyedValue = keyedValues.Length > 0 ? keyedValues[0] : null;
 103
 104                    // Check if this service has an interceptor proxy (its name + "_InterceptorProxy" exists)
 18105                    var hasInterceptorProxy = interceptedServiceNames.Contains(shortName);
 106
 18107                    assemblyTypes.Add(new DiagnosticTypeInfo(
 18108                        typeName,
 18109                        shortName,
 18110                        lifetime,
 18111                        interfaces,
 18112                        dependencies,
 18113                        isDecorator,
 18114                        isPlugin,
 18115                        hasFactoryAttr,
 18116                        keyedValue,
 18117                        isInterceptor: hasInterceptAttr,
 18118                        hasInterceptorProxy: hasInterceptorProxy));
 119                }
 120
 14121                if (assemblyTypes.Count > 0)
 122                {
 14123                    result[assemblySymbol.Name] = assemblyTypes;
 124                }
 125            }
 126        }
 127
 95128        return result;
 129    }
 130
 131    /// <summary>
 132    /// Discovers types from referenced assemblies with [GenerateTypeRegistry] for graph export.
 133    /// Unlike the main discovery, this includes internal types since they are registered by their own TypeRegistry.
 134    /// Returns DiscoveredType objects that can be included in the graph export.
 135    /// </summary>
 136    internal static Dictionary<string, IReadOnlyList<DiscoveredType>> DiscoverReferencedAssemblyTypesForGraph(Compilatio
 137    {
 4138        var result = new Dictionary<string, IReadOnlyList<DiscoveredType>>();
 139
 1360140        foreach (var reference in compilation.References)
 141        {
 676142            if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assemblySymbol)
 143            {
 144                // Skip the current assembly
 676145                if (SymbolEqualityComparer.Default.Equals(assemblySymbol, compilation.Assembly))
 146                    continue;
 147
 676148                if (!TypeDiscoveryHelper.HasGenerateTypeRegistryAttribute(assemblySymbol))
 149                    continue;
 150
 151                // Try to get interface locations from the assembly's ServiceCatalog
 0152                var interfaceLocationLookup = GetInterfaceLocationsFromServiceCatalog(assemblySymbol);
 153
 0154                var assemblyTypes = new List<DiscoveredType>();
 155
 0156                foreach (var typeSymbol in TypeDiscoveryHelper.GetAllTypes(assemblySymbol.GlobalNamespace))
 157                {
 158                    // Check if it's a registerable type
 0159                    var hasFactoryAttr = FactoryDiscoveryHelper.HasGenerateFactoryAttribute(typeSymbol);
 160
 161                    // Skip types that are only factories (handled separately)
 0162                    if (hasFactoryAttr)
 163                        continue;
 164
 0165                    if (!TypeDiscoveryHelper.WouldBeInjectableIgnoringAccessibility(typeSymbol) &&
 0166                        !TypeDiscoveryHelper.WouldBePluginIgnoringAccessibility(typeSymbol, compilation.Assembly))
 167                        continue;
 168
 169                    // Skip decorators - they modify other services, not registered directly as services
 0170                    if (TypeDiscoveryHelper.HasDecoratorForAttribute(typeSymbol) ||
 0171                        OpenDecoratorDiscoveryHelper.HasOpenDecoratorForAttribute(typeSymbol))
 172                        continue;
 173
 0174                    var typeName = TypeDiscoveryHelper.GetFullyQualifiedName(typeSymbol);
 0175                    var interfaceSymbols = TypeDiscoveryHelper.GetRegisterableInterfaces(typeSymbol, compilation.Assembl
 0176                    var interfaces = interfaceSymbols
 0177                        .Select(i => TypeDiscoveryHelper.GetFullyQualifiedName(i))
 0178                        .ToArray();
 179
 180                    // Get interface locations from ServiceCatalog lookup, falling back to symbol locations
 0181                    var interfaceInfos = interfaceSymbols.Select(i =>
 0182                    {
 0183                        var ifaceFullName = TypeDiscoveryHelper.GetFullyQualifiedName(i);
 0184
 0185                        // First try the ServiceCatalog lookup
 0186                        if (interfaceLocationLookup.TryGetValue(ifaceFullName, out var catalogInfo))
 0187                        {
 0188                            return catalogInfo;
 0189                        }
 0190
 0191                        // Fall back to symbol locations (works for source references)
 0192                        var ifaceLocation = i.Locations.FirstOrDefault();
 0193                        var ifaceFilePath = ifaceLocation?.SourceTree?.FilePath;
 0194                        var ifaceLine = ifaceLocation?.GetLineSpan().StartLinePosition.Line + 1 ?? 0;
 0195                        return new InterfaceInfo(ifaceFullName, ifaceFilePath, ifaceLine);
 0196                    }).ToArray();
 197
 0198                    var lifetime = TypeDiscoveryHelper.DetermineLifetime(typeSymbol) ?? GeneratorLifetime.Singleton;
 0199                    var constructorParams = TypeDiscoveryHelper.GetBestConstructorParametersWithKeys(typeSymbol)?.ToArra
 0200                        ?? Array.Empty<TypeDiscoveryHelper.ConstructorParameterInfo>();
 0201                    var keyedValues = TypeDiscoveryHelper.GetKeyedServiceKeys(typeSymbol);
 0202                    var sourceFilePath = typeSymbol.Locations.FirstOrDefault()?.SourceTree?.FilePath;
 0203                    var sourceLine = typeSymbol.Locations.FirstOrDefault() is { } location
 0204                        ? location.GetLineSpan().StartLinePosition.Line + 1
 0205                        : 0;
 206
 0207                    var discoveredType = new DiscoveredType(
 0208                        typeName,
 0209                        interfaces,
 0210                        assemblySymbol.Name,
 0211                        lifetime,
 0212                        constructorParams,
 0213                        keyedValues,
 0214                        sourceFilePath,
 0215                        sourceLine,
 0216                        TypeDiscoveryHelper.IsDisposableType(typeSymbol),
 0217                        interfaceInfos);
 218
 0219                    assemblyTypes.Add(discoveredType);
 220                }
 221
 0222                if (assemblyTypes.Count > 0)
 223                {
 0224                    result[assemblySymbol.Name] = assemblyTypes;
 225                }
 226            }
 227        }
 228
 4229        return result;
 230    }
 231
 232    /// <summary>
 233    /// Extracts interface location information from a referenced assembly's ServiceCatalog.
 234    /// The ServiceCatalog is generated by Needlr and contains compile-time interface location data.
 235    /// </summary>
 236    internal static Dictionary<string, InterfaceInfo> GetInterfaceLocationsFromServiceCatalog(IAssemblySymbol assemblySy
 237    {
 0238        var result = new Dictionary<string, InterfaceInfo>(StringComparer.Ordinal);
 239
 240        // Look for the ServiceCatalog class in the Generated namespace
 0241        var serviceCatalogTypeName = $"{assemblySymbol.Name}.Generated.ServiceCatalog";
 0242        var serviceCatalogType = assemblySymbol.GetTypeByMetadataName(serviceCatalogTypeName);
 243
 0244        if (serviceCatalogType == null)
 0245            return result;
 246
 247        // Find the Services property
 0248        var servicesProperty = serviceCatalogType.GetMembers("Services")
 0249            .OfType<IPropertySymbol>()
 0250            .FirstOrDefault();
 251
 0252        if (servicesProperty == null)
 0253            return result;
 254
 255        // The Services property has an initializer with ServiceCatalogEntry array
 256        // We need to parse the initializer to extract interface locations
 257        // This requires looking at the declaring syntax reference
 0258        var syntaxRef = servicesProperty.DeclaringSyntaxReferences.FirstOrDefault();
 0259        if (syntaxRef == null)
 0260            return result;
 261
 0262        var syntax = syntaxRef.GetSyntax();
 0263        if (syntax == null)
 0264            return result;
 265
 266        // Parse the array initializer to extract InterfaceEntry data
 267        // The format is: new InterfaceEntry("fullName", "filePath", line)
 0268        var text = syntax.ToFullString();
 269
 270        // Use regex to extract InterfaceEntry values
 0271        var interfaceEntryPattern = new System.Text.RegularExpressions.Regex(
 0272            @"new\s+global::NexusLabs\.Needlr\.Catalog\.InterfaceEntry\(\s*""([^""]+)""\s*,\s*(""([^""]+)""|null)\s*,\s*
 0273            System.Text.RegularExpressions.RegexOptions.Compiled);
 274
 0275        foreach (System.Text.RegularExpressions.Match match in interfaceEntryPattern.Matches(text))
 276        {
 0277            var fullName = match.Groups[1].Value;
 0278            var filePath = match.Groups[3].Success ? match.Groups[3].Value : null;
 0279            var line = int.Parse(match.Groups[4].Value);
 280
 0281            if (!result.ContainsKey(fullName))
 282            {
 0283                result[fullName] = new InterfaceInfo(fullName, filePath, line);
 284            }
 285        }
 286
 0287        return result;
 288    }
 289}