| | | 1 | | using System.Collections.Generic; |
| | | 2 | | using System.Linq; |
| | | 3 | | using System.Text; |
| | | 4 | | |
| | | 5 | | using NexusLabs.Needlr.Generators.Models; |
| | | 6 | | |
| | | 7 | | namespace NexusLabs.Needlr.Generators.CodeGen; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Composes normalized constructor guards, emits their direct calls, and writes XML |
| | | 11 | | /// exception documentation for built-in guards. |
| | | 12 | | /// </summary> |
| | | 13 | | internal static class ConstructorGuardCodeGenerator |
| | | 14 | | { |
| | | 15 | | /// <summary> |
| | | 16 | | /// Composes an optional default null guard with explicit guards, preserving order |
| | | 17 | | /// and removing duplicate effective calls. |
| | | 18 | | /// </summary> |
| | | 19 | | internal static ConstructorGuardModel[] ComposeEffectiveGuards( |
| | | 20 | | bool includeDefaultNotNull, |
| | | 21 | | ConstructorGuardModel[] explicitGuards) |
| | | 22 | | { |
| | 99 | 23 | | var suppressDefault = explicitGuards.Any( |
| | 151 | 24 | | guard => guard.Kind == GeneratedConstructorGuardKind.None); |
| | 99 | 25 | | var seen = new HashSet<( |
| | 99 | 26 | | GeneratedConstructorGuardKind Kind, |
| | 99 | 27 | | string? Type, |
| | 99 | 28 | | string? Method, |
| | 99 | 29 | | string ForwardedArguments)>(); |
| | 99 | 30 | | var guards = new List<ConstructorGuardModel>(); |
| | | 31 | | |
| | 99 | 32 | | if (includeDefaultNotNull && !suppressDefault && |
| | 99 | 33 | | seen.Add((GeneratedConstructorGuardKind.NotNull, null, null, string.Empty))) |
| | | 34 | | { |
| | 3 | 35 | | guards.Add(new ConstructorGuardModel( |
| | 3 | 36 | | GeneratedConstructorGuardKind.NotNull, |
| | 3 | 37 | | null, |
| | 3 | 38 | | null, |
| | 3 | 39 | | null)); |
| | | 40 | | } |
| | | 41 | | |
| | 304 | 42 | | foreach (var guard in explicitGuards) |
| | | 43 | | { |
| | 53 | 44 | | if (guard.Kind == GeneratedConstructorGuardKind.None) |
| | | 45 | | continue; |
| | | 46 | | |
| | 51 | 47 | | var key = ( |
| | 51 | 48 | | guard.Kind, |
| | 51 | 49 | | guard.CustomGuardTypeName, |
| | 51 | 50 | | guard.CustomGuardMethodName, |
| | 51 | 51 | | ForwardedArguments: string.Join("\u0001", guard.ForwardedArgumentLiterals)); |
| | 51 | 52 | | if (!seen.Add(key)) |
| | | 53 | | continue; |
| | | 54 | | |
| | 47 | 55 | | guards.Add(guard); |
| | | 56 | | } |
| | | 57 | | |
| | 99 | 58 | | return guards.ToArray(); |
| | | 59 | | } |
| | | 60 | | |
| | | 61 | | /// <summary> |
| | | 62 | | /// Returns whether an effective built-in guard rejects a runtime null value. |
| | | 63 | | /// </summary> |
| | | 64 | | internal static bool HasBuiltInNullRejectingGuard( |
| | | 65 | | IReadOnlyList<ConstructorGuardModel> effectiveGuards) |
| | | 66 | | { |
| | 12 | 67 | | return effectiveGuards.Any(guard => |
| | 19 | 68 | | guard.Kind == GeneratedConstructorGuardKind.NotNull || |
| | 19 | 69 | | guard.Kind == GeneratedConstructorGuardKind.NotNullOrEmpty || |
| | 19 | 70 | | guard.Kind == GeneratedConstructorGuardKind.NotNullOrWhiteSpace); |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | /// <summary> |
| | | 74 | | /// Builds the direct C# call for one normalized guard. |
| | | 75 | | /// </summary> |
| | | 76 | | internal static string BuildGuardCall( |
| | | 77 | | ConstructorGuardModel guard, |
| | | 78 | | string parameterName) |
| | | 79 | | { |
| | 50 | 80 | | return guard.Kind switch |
| | 50 | 81 | | { |
| | 50 | 82 | | GeneratedConstructorGuardKind.NotNull => |
| | 6 | 83 | | $"global::System.ArgumentNullException.ThrowIfNull({parameterName});", |
| | 50 | 84 | | GeneratedConstructorGuardKind.NotNullOrEmpty => |
| | 2 | 85 | | $"global::System.ArgumentException.ThrowIfNullOrEmpty({parameterName});", |
| | 50 | 86 | | GeneratedConstructorGuardKind.NotNullOrWhiteSpace => |
| | 8 | 87 | | $"global::System.ArgumentException.ThrowIfNullOrWhiteSpace({parameterName});", |
| | 50 | 88 | | GeneratedConstructorGuardKind.Custom => |
| | 34 | 89 | | BuildCustomGuardCall(guard, parameterName), |
| | 0 | 90 | | _ => string.Empty, |
| | 50 | 91 | | }; |
| | | 92 | | } |
| | | 93 | | |
| | | 94 | | /// <summary> |
| | | 95 | | /// Writes coalesced XML exception documentation for built-in guards. |
| | | 96 | | /// </summary> |
| | | 97 | | internal static void WriteExceptionDocumentation( |
| | | 98 | | StringBuilder builder, |
| | | 99 | | IReadOnlyList<string> parameterNames, |
| | | 100 | | IReadOnlyList<ConstructorGuardModel[]> effectiveGuards, |
| | | 101 | | string indentation) |
| | | 102 | | { |
| | 82 | 103 | | var nullParameters = new List<string>(); |
| | 82 | 104 | | var emptyParameters = new List<string>(); |
| | 82 | 105 | | var whiteSpaceParameters = new List<string>(); |
| | | 106 | | |
| | 354 | 107 | | for (var i = 0; i < parameterNames.Count; i++) |
| | | 108 | | { |
| | 95 | 109 | | var documentsNullFailure = false; |
| | 95 | 110 | | var documentsEmptyFailure = false; |
| | 95 | 111 | | var documentsWhiteSpaceFailure = false; |
| | | 112 | | |
| | 290 | 113 | | foreach (var guard in effectiveGuards[i]) |
| | | 114 | | { |
| | 50 | 115 | | switch (guard.Kind) |
| | | 116 | | { |
| | | 117 | | case GeneratedConstructorGuardKind.NotNull: |
| | 6 | 118 | | documentsNullFailure = true; |
| | 6 | 119 | | break; |
| | | 120 | | case GeneratedConstructorGuardKind.NotNullOrEmpty: |
| | 2 | 121 | | documentsNullFailure = true; |
| | 2 | 122 | | documentsEmptyFailure = true; |
| | 2 | 123 | | break; |
| | | 124 | | case GeneratedConstructorGuardKind.NotNullOrWhiteSpace: |
| | 8 | 125 | | documentsNullFailure = true; |
| | 8 | 126 | | documentsWhiteSpaceFailure = true; |
| | | 127 | | break; |
| | | 128 | | } |
| | | 129 | | } |
| | | 130 | | |
| | 95 | 131 | | if (documentsNullFailure) |
| | 14 | 132 | | nullParameters.Add(parameterNames[i]); |
| | | 133 | | |
| | 95 | 134 | | if (documentsWhiteSpaceFailure) |
| | 8 | 135 | | whiteSpaceParameters.Add(parameterNames[i]); |
| | 87 | 136 | | else if (documentsEmptyFailure) |
| | 2 | 137 | | emptyParameters.Add(parameterNames[i]); |
| | | 138 | | } |
| | | 139 | | |
| | 82 | 140 | | if (nullParameters.Count > 0) |
| | | 141 | | { |
| | 12 | 142 | | var description = BuildParameterCondition( |
| | 12 | 143 | | nullParameters, |
| | 12 | 144 | | "is <see langword=\"null\"/>."); |
| | 12 | 145 | | WriteExceptionElement( |
| | 12 | 146 | | builder, |
| | 12 | 147 | | indentation, |
| | 12 | 148 | | "global::System.ArgumentNullException", |
| | 12 | 149 | | description); |
| | | 150 | | } |
| | | 151 | | |
| | 82 | 152 | | if (emptyParameters.Count == 0 && whiteSpaceParameters.Count == 0) |
| | 73 | 153 | | return; |
| | | 154 | | |
| | 9 | 155 | | var descriptions = new List<string>(); |
| | 9 | 156 | | if (emptyParameters.Count > 0) |
| | | 157 | | { |
| | 2 | 158 | | descriptions.Add(BuildParameterCondition( |
| | 2 | 159 | | emptyParameters, |
| | 2 | 160 | | "is empty")); |
| | | 161 | | } |
| | | 162 | | |
| | 9 | 163 | | if (whiteSpaceParameters.Count > 0) |
| | | 164 | | { |
| | 8 | 165 | | descriptions.Add(BuildParameterCondition( |
| | 8 | 166 | | whiteSpaceParameters, |
| | 8 | 167 | | "is empty or consists only of white-space characters")); |
| | | 168 | | } |
| | | 169 | | |
| | 9 | 170 | | WriteExceptionElement( |
| | 9 | 171 | | builder, |
| | 9 | 172 | | indentation, |
| | 9 | 173 | | "global::System.ArgumentException", |
| | 9 | 174 | | string.Join("; or ", descriptions) + "."); |
| | 9 | 175 | | } |
| | | 176 | | |
| | | 177 | | private static string BuildCustomGuardCall( |
| | | 178 | | ConstructorGuardModel guard, |
| | | 179 | | string parameterName) |
| | | 180 | | { |
| | 34 | 181 | | var argumentList = guard.ForwardedArgumentLiterals.Length == 0 |
| | 34 | 182 | | ? parameterName |
| | 34 | 183 | | : $"{parameterName}, {string.Join(", ", guard.ForwardedArgumentLiterals)}"; |
| | | 184 | | |
| | 34 | 185 | | return $"{guard.CustomGuardTypeName}.{guard.CustomGuardMethodName}({argumentList}, nameof({parameterName}));"; |
| | | 186 | | } |
| | | 187 | | |
| | | 188 | | private static string BuildParameterCondition( |
| | | 189 | | IReadOnlyList<string> parameterNames, |
| | | 190 | | string condition) |
| | | 191 | | { |
| | 22 | 192 | | return $"{FormatParameterReferences(parameterNames)} {condition}"; |
| | | 193 | | } |
| | | 194 | | |
| | | 195 | | private static string FormatParameterReferences(IReadOnlyList<string> parameterNames) |
| | | 196 | | { |
| | 22 | 197 | | if (parameterNames.Count == 1) |
| | 21 | 198 | | return $"<paramref name=\"{parameterNames[0]}\"/>"; |
| | | 199 | | |
| | 1 | 200 | | if (parameterNames.Count == 2) |
| | | 201 | | { |
| | 0 | 202 | | return $"<paramref name=\"{parameterNames[0]}\"/> or <paramref name=\"{parameterNames[1]}\"/>"; |
| | | 203 | | } |
| | | 204 | | |
| | 1 | 205 | | var references = parameterNames |
| | 3 | 206 | | .Select(parameterName => $"<paramref name=\"{parameterName}\"/>") |
| | 1 | 207 | | .ToArray(); |
| | 1 | 208 | | return string.Join(", ", references.Take(references.Length - 1)) + |
| | 1 | 209 | | $", or {references[references.Length - 1]}"; |
| | | 210 | | } |
| | | 211 | | |
| | | 212 | | private static void WriteExceptionElement( |
| | | 213 | | StringBuilder builder, |
| | | 214 | | string indentation, |
| | | 215 | | string exceptionTypeName, |
| | | 216 | | string description) |
| | | 217 | | { |
| | 21 | 218 | | builder.AppendLine( |
| | 21 | 219 | | $"{indentation}/// <exception cref=\"{exceptionTypeName}\">"); |
| | 21 | 220 | | builder.AppendLine($"{indentation}/// {description}"); |
| | 21 | 221 | | builder.AppendLine($"{indentation}/// </exception>"); |
| | 21 | 222 | | } |
| | | 223 | | } |