| | | 1 | | using Microsoft.CodeAnalysis; |
| | | 2 | | |
| | | 3 | | namespace NexusLabs.Needlr.Generators; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Helper for discovering Options attributes from Roslyn symbols. |
| | | 7 | | /// </summary> |
| | | 8 | | internal 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 | | { |
| | 182 | 20 | | SectionName = sectionName; |
| | 182 | 21 | | Name = name; |
| | 182 | 22 | | ValidateOnStart = validateOnStart; |
| | 182 | 23 | | ValidateMethod = validateMethod; |
| | 182 | 24 | | ValidatorType = validatorType; |
| | 182 | 25 | | } |
| | | 26 | | |
| | | 27 | | /// <summary>Explicit section name from attribute, or null to infer from class name.</summary> |
| | 182 | 28 | | public string? SectionName { get; } |
| | | 29 | | |
| | | 30 | | /// <summary>Named options name (e.g., "Primary"), or null for default options.</summary> |
| | 182 | 31 | | public string? Name { get; } |
| | | 32 | | |
| | | 33 | | /// <summary>Whether to validate options on startup.</summary> |
| | 182 | 34 | | public bool ValidateOnStart { get; } |
| | | 35 | | |
| | | 36 | | /// <summary>Custom validation method name, or null to use convention ("Validate").</summary> |
| | 546 | 37 | | public string? ValidateMethod { get; } |
| | | 38 | | |
| | | 39 | | /// <summary>External validator type, or null to use the options class itself.</summary> |
| | 182 | 40 | | 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 | | { |
| | 7600860 | 50 | | foreach (var attribute in typeSymbol.GetAttributes()) |
| | | 51 | | { |
| | 2218615 | 52 | | var attributeClass = attribute.AttributeClass; |
| | 2218615 | 53 | | if (attributeClass == null) |
| | | 54 | | continue; |
| | | 55 | | |
| | 2218615 | 56 | | var name = attributeClass.Name; |
| | 2218615 | 57 | | if (name == OptionsAttributeName) |
| | 176 | 58 | | return true; |
| | | 59 | | |
| | 2218439 | 60 | | var fullName = attributeClass.ToDisplayString(); |
| | 2218439 | 61 | | if (fullName == OptionsAttributeFullName) |
| | 0 | 62 | | return true; |
| | | 63 | | } |
| | | 64 | | |
| | 1581727 | 65 | | 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 | | { |
| | 176 | 75 | | var result = new List<OptionsAttributeInfo>(); |
| | | 76 | | |
| | 716 | 77 | | foreach (var attribute in typeSymbol.GetAttributes()) |
| | | 78 | | { |
| | 182 | 79 | | var attributeClass = attribute.AttributeClass; |
| | 182 | 80 | | if (attributeClass == null) |
| | | 81 | | continue; |
| | | 82 | | |
| | 182 | 83 | | var name = attributeClass.Name; |
| | 182 | 84 | | var fullName = attributeClass.ToDisplayString(); |
| | | 85 | | |
| | 182 | 86 | | if (name != OptionsAttributeName && fullName != OptionsAttributeFullName) |
| | | 87 | | continue; |
| | | 88 | | |
| | | 89 | | // Extract constructor argument (optional section name) |
| | 182 | 90 | | string? sectionName = null; |
| | 182 | 91 | | if (attribute.ConstructorArguments.Length > 0 && |
| | 182 | 92 | | attribute.ConstructorArguments[0].Value is string section) |
| | | 93 | | { |
| | 91 | 94 | | sectionName = section; |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | // Extract named arguments |
| | 182 | 98 | | string? optionsName = null; |
| | 182 | 99 | | bool validateOnStart = false; |
| | 182 | 100 | | string? validateMethod = null; |
| | 182 | 101 | | INamedTypeSymbol? validatorType = null; |
| | | 102 | | |
| | 536 | 103 | | foreach (var namedArg in attribute.NamedArguments) |
| | | 104 | | { |
| | 86 | 105 | | if (namedArg.Key == "Name" && namedArg.Value.Value is string n) |
| | | 106 | | { |
| | 22 | 107 | | optionsName = n; |
| | | 108 | | } |
| | 64 | 109 | | else if (namedArg.Key == "ValidateOnStart" && namedArg.Value.Value is bool v) |
| | | 110 | | { |
| | 50 | 111 | | validateOnStart = v; |
| | | 112 | | } |
| | 14 | 113 | | else if (namedArg.Key == "ValidateMethod" && namedArg.Value.Value is string vm) |
| | | 114 | | { |
| | 3 | 115 | | validateMethod = vm; |
| | | 116 | | } |
| | 11 | 117 | | else if (namedArg.Key == "Validator" && namedArg.Value.Value is INamedTypeSymbol vt) |
| | | 118 | | { |
| | 10 | 119 | | validatorType = vt; |
| | | 120 | | } |
| | | 121 | | } |
| | | 122 | | |
| | 182 | 123 | | result.Add(new OptionsAttributeInfo(sectionName, optionsName, validateOnStart, validateMethod, validatorType |
| | | 124 | | } |
| | | 125 | | |
| | 176 | 126 | | 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<TOptions></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 | | { |
| | 388 | 149 | | foreach (var method in GetValidationMethods(typeSymbol, methodName)) |
| | | 150 | | { |
| | 22 | 151 | | if (GetValidationMethodSignatureError( |
| | 22 | 152 | | method, |
| | 22 | 153 | | optionsType, |
| | 22 | 154 | | isExternalValidator) is not null) |
| | | 155 | | { |
| | | 156 | | continue; |
| | | 157 | | } |
| | | 158 | | |
| | 20 | 159 | | if (isExternalValidator && |
| | 20 | 160 | | !SymbolEqualityComparer.Default.Equals( |
| | 20 | 161 | | method.Parameters[0].Type, |
| | 20 | 162 | | optionsType)) |
| | | 163 | | { |
| | | 164 | | continue; |
| | | 165 | | } |
| | | 166 | | |
| | 20 | 167 | | return new OptionsValidatorMethodInfo( |
| | 20 | 168 | | method.Name, |
| | 20 | 169 | | method.IsStatic, |
| | 20 | 170 | | usesOptionsValidatorInterface: false); |
| | | 171 | | } |
| | | 172 | | |
| | 162 | 173 | | if (isExternalValidator && |
| | 162 | 174 | | allowInterfaceFallback && |
| | 162 | 175 | | GetIOptionsValidatorTypeArguments(typeSymbol).Any(typeArgument => |
| | 164 | 176 | | SymbolEqualityComparer.Default.Equals(typeArgument, optionsType))) |
| | | 177 | | { |
| | 2 | 178 | | return new OptionsValidatorMethodInfo( |
| | 2 | 179 | | "Validate", |
| | 2 | 180 | | isStatic: false, |
| | 2 | 181 | | usesOptionsValidatorInterface: true); |
| | | 182 | | } |
| | | 183 | | |
| | 160 | 184 | | return null; |
| | 20 | 185 | | } |
| | | 186 | | |
| | | 187 | | internal static IEnumerable<IMethodSymbol> GetValidationMethods( |
| | | 188 | | INamedTypeSymbol targetType, |
| | | 189 | | string methodName) |
| | | 190 | | { |
| | 3958 | 191 | | foreach (var member in targetType.GetMembers()) |
| | | 192 | | { |
| | 1781 | 193 | | if (member is IMethodSymbol method && |
| | 1781 | 194 | | method.Name == methodName && |
| | 1781 | 195 | | method.DeclaredAccessibility == Accessibility.Public && |
| | 1781 | 196 | | method.MethodKind == MethodKind.Ordinary) |
| | | 197 | | { |
| | 44 | 198 | | yield return method; |
| | | 199 | | } |
| | | 200 | | } |
| | 188 | 201 | | } |
| | | 202 | | |
| | | 203 | | internal static string? GetValidationMethodSignatureError( |
| | | 204 | | IMethodSymbol method, |
| | | 205 | | INamedTypeSymbol optionsType, |
| | | 206 | | bool isExternalValidator) |
| | | 207 | | { |
| | 57 | 208 | | if (method.ReturnType is not INamedTypeSymbol returnType || |
| | 57 | 209 | | returnType.OriginalDefinition.ToDisplayString() != |
| | 57 | 210 | | "System.Collections.Generic.IEnumerable<T>" || |
| | 57 | 211 | | returnType.TypeArguments.Length != 1) |
| | | 212 | | { |
| | 13 | 213 | | return "IEnumerable<ValidationError> or IEnumerable<string>"; |
| | | 214 | | } |
| | | 215 | | |
| | 44 | 216 | | var resultType = returnType.TypeArguments[0]; |
| | 44 | 217 | | if (resultType.SpecialType != SpecialType.System_String && |
| | 44 | 218 | | resultType.ToDisplayString() != |
| | 44 | 219 | | "NexusLabs.Needlr.Generators.ValidationError") |
| | | 220 | | { |
| | 2 | 221 | | return "IEnumerable<ValidationError> or IEnumerable<string>"; |
| | | 222 | | } |
| | | 223 | | |
| | 42 | 224 | | if (isExternalValidator) |
| | | 225 | | { |
| | 18 | 226 | | return method.Parameters.Length == 1 |
| | 18 | 227 | | ? null |
| | 18 | 228 | | : $"IEnumerable<ValidationError> {method.Name}({optionsType.Name} options)"; |
| | | 229 | | } |
| | | 230 | | |
| | 24 | 231 | | if (!method.IsStatic && method.Parameters.Length != 0) |
| | | 232 | | { |
| | 2 | 233 | | return $"IEnumerable<ValidationError> {method.Name}()"; |
| | | 234 | | } |
| | | 235 | | |
| | 22 | 236 | | if (method.IsStatic && |
| | 22 | 237 | | (method.Parameters.Length != 1 || |
| | 22 | 238 | | !SymbolEqualityComparer.Default.Equals( |
| | 22 | 239 | | method.Parameters[0].Type, |
| | 22 | 240 | | optionsType))) |
| | | 241 | | { |
| | 6 | 242 | | return $"static IEnumerable<ValidationError> {method.Name}({optionsType.Name} options)"; |
| | | 243 | | } |
| | | 244 | | |
| | 16 | 245 | | return null; |
| | | 246 | | } |
| | | 247 | | |
| | | 248 | | internal static IEnumerable<ITypeSymbol> GetIOptionsValidatorTypeArguments( |
| | | 249 | | INamedTypeSymbol validatorType) |
| | | 250 | | { |
| | 16 | 251 | | foreach (var iface in validatorType.AllInterfaces) |
| | | 252 | | { |
| | 4 | 253 | | if (iface.Name == "IOptionsValidator" && |
| | 4 | 254 | | iface.ContainingNamespace?.ToDisplayString() == |
| | 4 | 255 | | "NexusLabs.Needlr.Generators" && |
| | 4 | 256 | | iface.IsGenericType && |
| | 4 | 257 | | iface.TypeArguments.Length == 1) |
| | | 258 | | { |
| | 4 | 259 | | yield return iface.TypeArguments[0]; |
| | | 260 | | } |
| | | 261 | | } |
| | 3 | 262 | | } |
| | | 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 | | { |
| | 22 | 274 | | MethodName = methodName; |
| | 22 | 275 | | IsStatic = isStatic; |
| | 22 | 276 | | UsesOptionsValidatorInterface = usesOptionsValidatorInterface; |
| | 22 | 277 | | } |
| | | 278 | | |
| | 22 | 279 | | public string MethodName { get; } |
| | 22 | 280 | | public bool IsStatic { get; } |
| | 22 | 281 | | public bool UsesOptionsValidatorInterface { get; } |
| | | 282 | | } |
| | | 283 | | } |