< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.Generators.AsyncLocalScopedGenerator
Assembly: NexusLabs.Needlr.AgentFramework.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Generators/AsyncLocalScopedGenerator.cs
Line coverage
97%
Covered lines: 89
Uncovered lines: 2
Coverable lines: 91
Total lines: 160
Line coverage: 97.8%
Branch coverage
81%
Covered branches: 44
Total branches: 54
Branch coverage: 81.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Initialize(...)100%11100%
ExtractInfo(...)81.48%545497.18%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework.Generators/AsyncLocalScopedGenerator.cs

#LineLine coverage
 1using System.Collections.Immutable;
 2using System.Linq;
 3using System.Text;
 4
 5using Microsoft.CodeAnalysis;
 6using Microsoft.CodeAnalysis.CSharp.Syntax;
 7using Microsoft.CodeAnalysis.Text;
 8
 9using NexusLabs.Needlr.AgentFramework.Generators.CodeGen;
 10using NexusLabs.Needlr.AgentFramework.Generators.Models;
 11
 12namespace NexusLabs.Needlr.AgentFramework.Generators
 13{
 14    /// <summary>
 15    /// Source generator for [AsyncLocalScoped]-decorated interfaces.
 16    /// Emits an internal sealed class implementing the interface with proper
 17    /// AsyncLocal scoping and dispose semantics.
 18    /// </summary>
 19    [Generator]
 20    public class AsyncLocalScopedGenerator : IIncrementalGenerator
 21    {
 22        private const string AsyncLocalScopedAttributeName =
 23            "NexusLabs.Needlr.AgentFramework.AsyncLocalScopedAttribute";
 24
 25        public void Initialize(IncrementalGeneratorInitializationContext context)
 26        {
 1127            var interfaces = context.SyntaxProvider
 1128                .ForAttributeWithMetadataName(
 1129                    AsyncLocalScopedAttributeName,
 1130                    predicate: static (s, _) => s is InterfaceDeclarationSyntax,
 1031                    transform: static (ctx, ct) => ExtractInfo(ctx))
 1032                .Where(static m => m.HasValue)
 1933                .Select(static (m, _) => m!.Value);
 34
 1135            context.RegisterSourceOutput(interfaces, static (spc, info) =>
 1136            {
 837                var source = AsyncLocalScopedCodeGenerator.Generate(info);
 838                var safeName = info.InterfaceFullName
 839                    .Replace("global::", "")
 840                    .Replace(".", "_")
 841                    .Replace("<", "_")
 842                    .Replace(">", "_");
 1143
 844                spc.AddSource(safeName + ".AsyncLocalScoped.g.cs",
 845                    SourceText.From(source, Encoding.UTF8));
 1946            });
 1147        }
 48
 49        private static AsyncLocalScopedInfo? ExtractInfo(GeneratorAttributeSyntaxContext ctx)
 50        {
 1051            var typeSymbol = ctx.TargetSymbol as INamedTypeSymbol;
 1052            if (typeSymbol == null || typeSymbol.TypeKind != TypeKind.Interface)
 053                return null;
 54
 55            // Find the [AsyncLocalScoped] attribute and its Mutable property
 1056            var attrData = ctx.Attributes.FirstOrDefault(a =>
 2057                a.AttributeClass != null &&
 2058                a.AttributeClass.ToDisplayString() == AsyncLocalScopedAttributeName);
 59
 1060            if (attrData == null)
 061                return null;
 62
 1063            bool isMutable = false;
 3064            foreach (var named in attrData.NamedArguments)
 65            {
 566                if (named.Key == "Mutable" && named.Value.Value is bool b)
 567                    isMutable = b;
 68            }
 69
 70            // Find the "Current" property to determine the value type
 1071            var currentProp = typeSymbol.GetMembers()
 1072                .OfType<IPropertySymbol>()
 1973                .FirstOrDefault(p => p.Name == "Current");
 74
 1075            if (currentProp == null)
 176                return null;
 77
 78            // Get the non-nullable underlying type
 979            var valueType = currentProp.Type;
 980            if (valueType is INamedTypeSymbol namedType &&
 981                namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
 82            {
 283                valueType = namedType.TypeArguments[0];
 84            }
 785            else if (valueType.NullableAnnotation == NullableAnnotation.Annotated &&
 786                     valueType is INamedTypeSymbol annotatedType)
 87            {
 788                valueType = annotatedType;
 89            }
 90
 991            var valueTypeFullName = valueType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 92
 93            // Find the scope method (returns IDisposable)
 994            var scopeMethod = typeSymbol.GetMembers()
 995                .OfType<IMethodSymbol>()
 996                .FirstOrDefault(m =>
 2697                    m.ReturnType != null &&
 2698                    m.ReturnType.ToDisplayString().EndsWith("IDisposable"));
 99
 9100            if (scopeMethod == null)
 1101                return null;
 102
 8103            bool hasScopeParameter = scopeMethod.Parameters.Length > 0;
 8104            string scopeParameterTypeFullName = "";
 105
 8106            if (hasScopeParameter)
 107            {
 6108                var paramType = scopeMethod.Parameters[0].Type;
 6109                scopeParameterTypeFullName = paramType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 110            }
 111
 8112            var interfaceFullName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
 113
 8114            var namespaceName = typeSymbol.ContainingNamespace?.IsGlobalNamespace == true
 8115                ? ""
 8116                : typeSymbol.ContainingNamespace?.ToDisplayString() ?? "";
 117
 118            // Discover additional properties on the interface (beyond "Current")
 119            // that should be proxied through to Current?.PropertyName.
 8120            var proxyProps = ImmutableArray.CreateBuilder<Models.AsyncLocalScopedPropertyInfo>();
 106121            foreach (var member in typeSymbol.GetMembers())
 122            {
 45123                if (member is IPropertySymbol prop &&
 45124                    prop.Name != "Current" &&
 45125                    !prop.IsStatic &&
 45126                    !prop.IsIndexer)
 127                {
 8128                    var propTypeFullName = prop.Type.ToDisplayString(
 8129                        SymbolDisplayFormat.FullyQualifiedFormat
 8130                            .WithMiscellaneousOptions(
 8131                                SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier));
 132
 8133                    bool hasSetter = prop.SetMethod != null;
 134
 8135                    bool isNonNullableValueType = prop.Type.IsValueType &&
 8136                        prop.Type.NullableAnnotation != NullableAnnotation.Annotated &&
 8137                        !(prop.Type is INamedTypeSymbol nt &&
 8138                          nt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T);
 139
 8140                    proxyProps.Add(new Models.AsyncLocalScopedPropertyInfo(
 8141                        name: prop.Name,
 8142                        typeFullName: propTypeFullName,
 8143                        hasSetter: hasSetter,
 8144                        isNonNullableValueType: isNonNullableValueType));
 145                }
 146            }
 147
 8148            return new AsyncLocalScopedInfo(
 8149                interfaceFullName: interfaceFullName,
 8150                interfaceName: typeSymbol.Name,
 8151                namespaceName: namespaceName,
 8152                valueTypeFullName: valueTypeFullName,
 8153                scopeMethodName: scopeMethod.Name,
 8154                hasScopeParameter: hasScopeParameter,
 8155                scopeParameterTypeFullName: scopeParameterTypeFullName,
 8156                isMutable: isMutable,
 8157                proxyProperties: proxyProps.ToImmutable());
 158        }
 159    }
 160}