| | | 1 | | using System; |
| | | 2 | | using System.Globalization; |
| | | 3 | | using System.Linq; |
| | | 4 | | |
| | | 5 | | using Microsoft.CodeAnalysis; |
| | | 6 | | using Microsoft.CodeAnalysis.CSharp; |
| | | 7 | | |
| | | 8 | | namespace NexusLabs.Needlr.Generators; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Renders a Roslyn <see cref="TypedConstant"/> -- an attribute constructor argument |
| | | 12 | | /// value already validated by the compiler -- into the exact C# literal or expression |
| | | 13 | | /// source text needed to forward it as a positional argument on a generated guard |
| | | 14 | | /// method call. |
| | | 15 | | /// </summary> |
| | | 16 | | /// <remarks> |
| | | 17 | | /// <para> |
| | | 18 | | /// Only scalar constant shapes that can be rendered unambiguously and safely are |
| | | 19 | | /// supported: <see langword="null"/>, <see langword="bool"/>, every signed and |
| | | 20 | | /// unsigned integral primitive (with the C# suffix required for the literal to |
| | | 21 | | /// round-trip to the same runtime type, e.g. <c>u</c>/<c>L</c>/<c>UL</c>), |
| | | 22 | | /// <see langword="char"/>, <see langword="string"/>, enum members (rendered as a |
| | | 23 | | /// fully qualified member reference, or a fully qualified cast expression when no |
| | | 24 | | /// declared member matches the value exactly -- e.g. a combined <c>[Flags]</c> value), |
| | | 25 | | /// and <see cref="System.Type"/> (rendered as <c>typeof(global::...)</c>, including |
| | | 26 | | /// open generic type definitions such as <c>typeof(List<>)</c>). |
| | | 27 | | /// </para> |
| | | 28 | | /// <para> |
| | | 29 | | /// Arrays/params and floating-point (<see langword="float"/>/<see langword="double"/>) |
| | | 30 | | /// constants are deliberately unsupported in this slice: rendering an array positional |
| | | 31 | | /// argument as a single forwarded literal is inherently ambiguous, and floating-point |
| | | 32 | | /// literals can silently lose round-trip precision. A dedicated analyzer is |
| | | 33 | | /// responsible for diagnosing an alias attribute usage whose positional arguments |
| | | 34 | | /// include one of these unsupported shapes; this renderer only ever reports the |
| | | 35 | | /// unsupported result explicitly via its <see langword="bool"/> return value -- it |
| | | 36 | | /// never invents a success-shaped fallback string for a shape it cannot safely render. |
| | | 37 | | /// </para> |
| | | 38 | | /// <para> |
| | | 39 | | /// This type never accepts or echoes raw source text: every rendered literal is |
| | | 40 | | /// derived solely from the compiler-validated <see cref="TypedConstant"/> value, never |
| | | 41 | | /// from unparsed attribute-argument syntax. |
| | | 42 | | /// </para> |
| | | 43 | | /// </remarks> |
| | | 44 | | internal static class TypedConstantRenderer |
| | | 45 | | { |
| | | 46 | | /// <summary> |
| | | 47 | | /// Attempts to render <paramref name="constant"/> as a C# literal or expression |
| | | 48 | | /// suitable for splicing directly into generated source. Returns |
| | | 49 | | /// <see langword="false"/> -- with <paramref name="renderedLiteral"/> set to |
| | | 50 | | /// <see cref="string.Empty"/> -- for any constant shape this renderer does not |
| | | 51 | | /// support (arrays/params, floating-point primitives, or any other unrecognized |
| | | 52 | | /// shape), rather than falling back to a best-effort rendering that could produce |
| | | 53 | | /// unsafe or ambiguous source. |
| | | 54 | | /// </summary> |
| | | 55 | | internal static bool TryRender(TypedConstant constant, out string renderedLiteral) |
| | | 56 | | { |
| | 80 | 57 | | if (constant.IsNull) |
| | | 58 | | { |
| | 4 | 59 | | renderedLiteral = "null"; |
| | 4 | 60 | | return true; |
| | | 61 | | } |
| | | 62 | | |
| | 76 | 63 | | switch (constant.Kind) |
| | | 64 | | { |
| | | 65 | | case TypedConstantKind.Primitive: |
| | 62 | 66 | | return TryRenderPrimitive(constant.Value, out renderedLiteral); |
| | | 67 | | |
| | | 68 | | case TypedConstantKind.Enum: |
| | 5 | 69 | | return TryRenderEnum(constant, out renderedLiteral); |
| | | 70 | | |
| | | 71 | | case TypedConstantKind.Type: |
| | 5 | 72 | | return TryRenderType(constant, out renderedLiteral); |
| | | 73 | | |
| | | 74 | | default: |
| | | 75 | | // TypedConstantKind.Array and TypedConstantKind.Error (and any future |
| | | 76 | | // kind this renderer doesn't yet know about) are unsupported. Note |
| | | 77 | | // that TypedConstant.Value throws for an array constant, so Kind must |
| | | 78 | | // be checked -- and rejected -- before any Value access is attempted. |
| | 4 | 79 | | renderedLiteral = string.Empty; |
| | 4 | 80 | | return false; |
| | | 81 | | } |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | private static bool TryRenderPrimitive(object? value, out string renderedLiteral) |
| | | 85 | | { |
| | | 86 | | switch (value) |
| | | 87 | | { |
| | | 88 | | case bool boolValue: |
| | 1 | 89 | | renderedLiteral = boolValue ? "true" : "false"; |
| | 1 | 90 | | return true; |
| | | 91 | | |
| | | 92 | | case char charValue: |
| | 3 | 93 | | renderedLiteral = SymbolDisplay.FormatLiteral(charValue, quote: true); |
| | 3 | 94 | | return true; |
| | | 95 | | |
| | | 96 | | case string stringValue: |
| | 9 | 97 | | renderedLiteral = SymbolDisplay.FormatLiteral(stringValue, quote: true); |
| | 9 | 98 | | return true; |
| | | 99 | | |
| | | 100 | | case sbyte sbyteValue: |
| | 1 | 101 | | renderedLiteral = sbyteValue.ToString(CultureInfo.InvariantCulture); |
| | 1 | 102 | | return true; |
| | | 103 | | |
| | | 104 | | case byte byteValue: |
| | 1 | 105 | | renderedLiteral = byteValue.ToString(CultureInfo.InvariantCulture); |
| | 1 | 106 | | return true; |
| | | 107 | | |
| | | 108 | | case short shortValue: |
| | 1 | 109 | | renderedLiteral = shortValue.ToString(CultureInfo.InvariantCulture); |
| | 1 | 110 | | return true; |
| | | 111 | | |
| | | 112 | | case ushort ushortValue: |
| | 1 | 113 | | renderedLiteral = ushortValue.ToString(CultureInfo.InvariantCulture); |
| | 1 | 114 | | return true; |
| | | 115 | | |
| | | 116 | | case int intValue: |
| | 31 | 117 | | renderedLiteral = intValue.ToString(CultureInfo.InvariantCulture); |
| | 31 | 118 | | return true; |
| | | 119 | | |
| | | 120 | | case uint uintValue: |
| | 3 | 121 | | renderedLiteral = uintValue.ToString(CultureInfo.InvariantCulture) + "u"; |
| | 3 | 122 | | return true; |
| | | 123 | | |
| | | 124 | | case long longValue: |
| | 3 | 125 | | renderedLiteral = longValue.ToString(CultureInfo.InvariantCulture) + "L"; |
| | 3 | 126 | | return true; |
| | | 127 | | |
| | | 128 | | case ulong ulongValue: |
| | 3 | 129 | | renderedLiteral = ulongValue.ToString(CultureInfo.InvariantCulture) + "UL"; |
| | 3 | 130 | | return true; |
| | | 131 | | |
| | | 132 | | default: |
| | | 133 | | // float, double (explicitly excluded from this slice), and anything |
| | | 134 | | // else unrecognized (e.g. decimal, which is not a legal attribute |
| | | 135 | | // constructor parameter type in the first place). |
| | 5 | 136 | | renderedLiteral = string.Empty; |
| | 5 | 137 | | return false; |
| | | 138 | | } |
| | | 139 | | } |
| | | 140 | | |
| | | 141 | | private static bool TryRenderEnum(TypedConstant constant, out string renderedLiteral) |
| | | 142 | | { |
| | 5 | 143 | | if (constant.Type is not INamedTypeSymbol enumType) |
| | | 144 | | { |
| | 0 | 145 | | renderedLiteral = string.Empty; |
| | 0 | 146 | | return false; |
| | | 147 | | } |
| | | 148 | | |
| | 5 | 149 | | var qualifiedEnumTypeName = enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); |
| | | 150 | | |
| | 5 | 151 | | var matchingMember = enumType.GetMembers() |
| | 5 | 152 | | .OfType<IFieldSymbol>() |
| | 20 | 153 | | .FirstOrDefault(member => member.IsConst && Equals(member.ConstantValue, constant.Value)); |
| | | 154 | | |
| | 5 | 155 | | if (matchingMember is not null) |
| | | 156 | | { |
| | 4 | 157 | | renderedLiteral = $"{qualifiedEnumTypeName}.{matchingMember.Name}"; |
| | 4 | 158 | | return true; |
| | | 159 | | } |
| | | 160 | | |
| | | 161 | | // No single declared member matches the value exactly (for example, a |
| | | 162 | | // combined [Flags] value with no dedicated named member). A fully qualified |
| | | 163 | | // cast of the exact underlying value is still a safe, unambiguous rendering. |
| | 1 | 164 | | var underlyingValueLiteral = Convert.ToString(constant.Value, CultureInfo.InvariantCulture); |
| | 1 | 165 | | renderedLiteral = $"({qualifiedEnumTypeName}){underlyingValueLiteral}"; |
| | 1 | 166 | | return true; |
| | | 167 | | } |
| | | 168 | | |
| | | 169 | | private static bool TryRenderType(TypedConstant constant, out string renderedLiteral) |
| | | 170 | | { |
| | 5 | 171 | | if (constant.Value is not ITypeSymbol typeSymbol) |
| | | 172 | | { |
| | 0 | 173 | | renderedLiteral = string.Empty; |
| | 0 | 174 | | return false; |
| | | 175 | | } |
| | | 176 | | |
| | 5 | 177 | | renderedLiteral = $"typeof({typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; |
| | 5 | 178 | | return true; |
| | | 179 | | } |
| | | 180 | | } |