< Summary

Information
Class: NexusLabs.Needlr.Generators.OptionsAttributeHelper
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/OptionsAttributeHelper.cs
Line coverage
99%
Covered lines: 114
Uncovered lines: 1
Coverable lines: 115
Total lines: 283
Line coverage: 99.1%
Branch coverage
96%
Covered branches: 93
Total branches: 96
Branch coverage: 96.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_SectionName()100%11100%
get_Name()100%11100%
get_ValidateOnStart()100%11100%
get_ValidateMethod()100%11100%
get_ValidatorType()100%11100%
HasOptionsAttribute(...)87.5%8890%
GetOptionsAttributes(...)96.66%3030100%
FindValidationMethod(...)100%1212100%
GetValidationMethods()100%1010100%
GetValidationMethodSignatureError(...)100%2424100%
GetIOptionsValidatorTypeArguments()91.66%1212100%
.ctor(...)100%11100%
get_MethodName()100%11100%
get_IsStatic()100%11100%
get_UsesOptionsValidatorInterface()100%11100%

File(s)

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

#LineLine coverage
 1using Microsoft.CodeAnalysis;
 2
 3namespace NexusLabs.Needlr.Generators;
 4
 5/// <summary>
 6/// Helper for discovering Options attributes from Roslyn symbols.
 7/// </summary>
 8internal static class OptionsAttributeHelper
 9{
 10    private const string OptionsAttributeName = "OptionsAttribute";
 11    private const string OptionsAttributeFullName = "NexusLabs.Needlr.OptionsAttribute";
 12
 13    /// <summary>
 14    /// Information extracted from an [Options] attribute.
 15    /// </summary>
 16    public readonly struct OptionsAttributeInfo
 17    {
 18        public OptionsAttributeInfo(string? sectionName, string? name, bool validateOnStart, string? validateMethod = nu
 19        {
 18220            SectionName = sectionName;
 18221            Name = name;
 18222            ValidateOnStart = validateOnStart;
 18223            ValidateMethod = validateMethod;
 18224            ValidatorType = validatorType;
 18225        }
 26
 27        /// <summary>Explicit section name from attribute, or null to infer from class name.</summary>
 18228        public string? SectionName { get; }
 29
 30        /// <summary>Named options name (e.g., "Primary"), or null for default options.</summary>
 18231        public string? Name { get; }
 32
 33        /// <summary>Whether to validate options on startup.</summary>
 18234        public bool ValidateOnStart { get; }
 35
 36        /// <summary>Custom validation method name, or null to use convention ("Validate").</summary>
 54637        public string? ValidateMethod { get; }
 38
 39        /// <summary>External validator type, or null to use the options class itself.</summary>
 18240        public INamedTypeSymbol? ValidatorType { get; }
 41    }
 42
 43    /// <summary>
 44    /// Checks if a type has the [Options] attribute.
 45    /// </summary>
 46    /// <param name="typeSymbol">The type symbol to check.</param>
 47    /// <returns>True if the type has [Options]; otherwise, false.</returns>
 48    public static bool HasOptionsAttribute(INamedTypeSymbol typeSymbol)
 49    {
 760086050        foreach (var attribute in typeSymbol.GetAttributes())
 51        {
 221861552            var attributeClass = attribute.AttributeClass;
 221861553            if (attributeClass == null)
 54                continue;
 55
 221861556            var name = attributeClass.Name;
 221861557            if (name == OptionsAttributeName)
 17658                return true;
 59
 221843960            var fullName = attributeClass.ToDisplayString();
 221843961            if (fullName == OptionsAttributeFullName)
 062                return true;
 63        }
 64
 158172765        return false;
 66    }
 67
 68    /// <summary>
 69    /// Gets all [Options] attribute data from a type.
 70    /// </summary>
 71    /// <param name="typeSymbol">The type symbol to check.</param>
 72    /// <returns>A list of options attribute info for each [Options] on the type.</returns>
 73    public static IReadOnlyList<OptionsAttributeInfo> GetOptionsAttributes(INamedTypeSymbol typeSymbol)
 74    {
 17675        var result = new List<OptionsAttributeInfo>();
 76
 71677        foreach (var attribute in typeSymbol.GetAttributes())
 78        {
 18279            var attributeClass = attribute.AttributeClass;
 18280            if (attributeClass == null)
 81                continue;
 82
 18283            var name = attributeClass.Name;
 18284            var fullName = attributeClass.ToDisplayString();
 85
 18286            if (name != OptionsAttributeName && fullName != OptionsAttributeFullName)
 87                continue;
 88
 89            // Extract constructor argument (optional section name)
 18290            string? sectionName = null;
 18291            if (attribute.ConstructorArguments.Length > 0 &&
 18292                attribute.ConstructorArguments[0].Value is string section)
 93            {
 9194                sectionName = section;
 95            }
 96
 97            // Extract named arguments
 18298            string? optionsName = null;
 18299            bool validateOnStart = false;
 182100            string? validateMethod = null;
 182101            INamedTypeSymbol? validatorType = null;
 102
 536103            foreach (var namedArg in attribute.NamedArguments)
 104            {
 86105                if (namedArg.Key == "Name" && namedArg.Value.Value is string n)
 106                {
 22107                    optionsName = n;
 108                }
 64109                else if (namedArg.Key == "ValidateOnStart" && namedArg.Value.Value is bool v)
 110                {
 50111                    validateOnStart = v;
 112                }
 14113                else if (namedArg.Key == "ValidateMethod" && namedArg.Value.Value is string vm)
 114                {
 3115                    validateMethod = vm;
 116                }
 11117                else if (namedArg.Key == "Validator" && namedArg.Value.Value is INamedTypeSymbol vt)
 118                {
 10119                    validatorType = vt;
 120                }
 121            }
 122
 182123            result.Add(new OptionsAttributeInfo(sectionName, optionsName, validateOnStart, validateMethod, validatorType
 124        }
 125
 176126        return result;
 127    }
 128
 129    /// <summary>
 130    /// Finds a validation method on a type by convention or explicit name.
 131    /// Convention: method named "Validate" (or custom name via ValidateMethod property).
 132    /// </summary>
 133    /// <param name="typeSymbol">The type symbol to search.</param>
 134    /// <param name="optionsType">The options type accepted by the validation method.</param>
 135    /// <param name="methodName">The method name to look for.</param>
 136    /// <param name="isExternalValidator">Whether the target type is an external validator.</param>
 137    /// <param name="allowInterfaceFallback">
 138    /// Whether a matching <c>IOptionsValidator&lt;TOptions&gt;</c> implementation may be used
 139    /// when no public validation method is available.
 140    /// </param>
 141    /// <returns>Validator method info, or null if no validator method found.</returns>
 142    internal static OptionsValidatorMethodInfo? FindValidationMethod(
 143        INamedTypeSymbol typeSymbol,
 144        INamedTypeSymbol optionsType,
 145        string methodName,
 146        bool isExternalValidator,
 147        bool allowInterfaceFallback)
 148    {
 388149        foreach (var method in GetValidationMethods(typeSymbol, methodName))
 150        {
 22151            if (GetValidationMethodSignatureError(
 22152                    method,
 22153                    optionsType,
 22154                    isExternalValidator) is not null)
 155            {
 156                continue;
 157            }
 158
 20159            if (isExternalValidator &&
 20160                !SymbolEqualityComparer.Default.Equals(
 20161                    method.Parameters[0].Type,
 20162                    optionsType))
 163            {
 164                continue;
 165            }
 166
 20167            return new OptionsValidatorMethodInfo(
 20168                method.Name,
 20169                method.IsStatic,
 20170                usesOptionsValidatorInterface: false);
 171        }
 172
 162173        if (isExternalValidator &&
 162174            allowInterfaceFallback &&
 162175            GetIOptionsValidatorTypeArguments(typeSymbol).Any(typeArgument =>
 164176                SymbolEqualityComparer.Default.Equals(typeArgument, optionsType)))
 177        {
 2178            return new OptionsValidatorMethodInfo(
 2179                "Validate",
 2180                isStatic: false,
 2181                usesOptionsValidatorInterface: true);
 182        }
 183
 160184        return null;
 20185    }
 186
 187    internal static IEnumerable<IMethodSymbol> GetValidationMethods(
 188        INamedTypeSymbol targetType,
 189        string methodName)
 190    {
 3958191        foreach (var member in targetType.GetMembers())
 192        {
 1781193            if (member is IMethodSymbol method &&
 1781194                method.Name == methodName &&
 1781195                method.DeclaredAccessibility == Accessibility.Public &&
 1781196                method.MethodKind == MethodKind.Ordinary)
 197            {
 44198                yield return method;
 199            }
 200        }
 188201    }
 202
 203    internal static string? GetValidationMethodSignatureError(
 204        IMethodSymbol method,
 205        INamedTypeSymbol optionsType,
 206        bool isExternalValidator)
 207    {
 57208        if (method.ReturnType is not INamedTypeSymbol returnType ||
 57209            returnType.OriginalDefinition.ToDisplayString() !=
 57210                "System.Collections.Generic.IEnumerable<T>" ||
 57211            returnType.TypeArguments.Length != 1)
 212        {
 13213            return "IEnumerable<ValidationError> or IEnumerable<string>";
 214        }
 215
 44216        var resultType = returnType.TypeArguments[0];
 44217        if (resultType.SpecialType != SpecialType.System_String &&
 44218            resultType.ToDisplayString() !=
 44219                "NexusLabs.Needlr.Generators.ValidationError")
 220        {
 2221            return "IEnumerable<ValidationError> or IEnumerable<string>";
 222        }
 223
 42224        if (isExternalValidator)
 225        {
 18226            return method.Parameters.Length == 1
 18227                ? null
 18228                : $"IEnumerable<ValidationError> {method.Name}({optionsType.Name} options)";
 229        }
 230
 24231        if (!method.IsStatic && method.Parameters.Length != 0)
 232        {
 2233            return $"IEnumerable<ValidationError> {method.Name}()";
 234        }
 235
 22236        if (method.IsStatic &&
 22237            (method.Parameters.Length != 1 ||
 22238             !SymbolEqualityComparer.Default.Equals(
 22239                 method.Parameters[0].Type,
 22240                 optionsType)))
 241        {
 6242            return $"static IEnumerable<ValidationError> {method.Name}({optionsType.Name} options)";
 243        }
 244
 16245        return null;
 246    }
 247
 248    internal static IEnumerable<ITypeSymbol> GetIOptionsValidatorTypeArguments(
 249        INamedTypeSymbol validatorType)
 250    {
 16251        foreach (var iface in validatorType.AllInterfaces)
 252        {
 4253            if (iface.Name == "IOptionsValidator" &&
 4254                iface.ContainingNamespace?.ToDisplayString() ==
 4255                    "NexusLabs.Needlr.Generators" &&
 4256                iface.IsGenericType &&
 4257                iface.TypeArguments.Length == 1)
 258            {
 4259                yield return iface.TypeArguments[0];
 260            }
 261        }
 3262    }
 263
 264    /// <summary>
 265    /// Information about a validation method.
 266    /// </summary>
 267    public readonly struct OptionsValidatorMethodInfo
 268    {
 269        public OptionsValidatorMethodInfo(
 270            string methodName,
 271            bool isStatic,
 272            bool usesOptionsValidatorInterface)
 273        {
 22274            MethodName = methodName;
 22275            IsStatic = isStatic;
 22276            UsesOptionsValidatorInterface = usesOptionsValidatorInterface;
 22277        }
 278
 22279        public string MethodName { get; }
 22280        public bool IsStatic { get; }
 22281        public bool UsesOptionsValidatorInterface { get; }
 282    }
 283}