< Summary

Information
Class: NexusLabs.Needlr.Generators.GeneratorHelpers
Assembly: NexusLabs.Needlr.Generators
File(s): /actions-runner/_work/needlr/needlr/src/NexusLabs.Needlr.Generators/GeneratorHelpers.cs
Line coverage
92%
Covered lines: 119
Uncovered lines: 10
Coverable lines: 129
Total lines: 368
Line coverage: 92.2%
Branch coverage
83%
Covered branches: 93
Total branches: 112
Branch coverage: 83%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/actions-runner/_work/needlr/needlr/src/NexusLabs.Needlr.Generators/GeneratorHelpers.cs

#LineLine coverage
 1// Copyright (c) NexusLabs. All rights reserved.
 2// Licensed under the MIT License.
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Linq;
 7using System.Text;
 8
 9using Microsoft.CodeAnalysis.CSharp;
 10
 11namespace NexusLabs.Needlr.Generators;
 12
 13/// <summary>
 14/// Utility methods for source code generation.
 15/// </summary>
 16internal static class GeneratorHelpers
 17{
 18    /// <summary>
 19    /// Sanitizes an assembly name to be a valid C# identifier for use in namespaces.
 20    /// </summary>
 21    public static string SanitizeIdentifier(string name)
 22    {
 9650323        if (string.IsNullOrEmpty(name))
 024            return "Generated";
 25
 9650326        var sb = new StringBuilder(name.Length);
 489629027        foreach (var c in name)
 28        {
 235164229            if (char.IsLetterOrDigit(c) || c == '_')
 30            {
 216402631                sb.Append(c);
 32            }
 18761633            else if (c == '.' || c == '-' || c == ' ')
 34            {
 35                // Keep dots for namespace segments, replace dashes/spaces with underscores
 18761636                sb.Append(c == '.' ? '.' : '_');
 37            }
 38            // Skip other characters
 39        }
 40
 9650341        var result = sb.ToString();
 42
 43        // Ensure each segment doesn't start with a digit
 9650344        var segments = result.Split('.');
 76124445        for (int i = 0; i < segments.Length; i++)
 46        {
 28411947            if (segments[i].Length > 0 && char.IsDigit(segments[i][0]))
 48            {
 049                segments[i] = "_" + segments[i];
 50            }
 51        }
 52
 38062253        return string.Join(".", segments.Where(s => s.Length > 0));
 54    }
 55
 56    /// <summary>
 57    /// Escapes a C# keyword or contextual keyword for use as an identifier.
 58    /// </summary>
 59    public static string EscapeIdentifier(string name)
 60    {
 53261        return SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None ||
 53262            SyntaxFacts.GetContextualKeywordKind(name) != SyntaxKind.None
 53263                ? "@" + name
 53264                : name;
 65    }
 66
 67    /// <summary>
 68    /// Escapes a string for use in a regular C# string literal.
 69    /// </summary>
 70    public static string EscapeStringLiteral(string value)
 71    {
 95410772        if (string.IsNullOrEmpty(value))
 183673            return string.Empty;
 95227174        return value.Replace("\\", "\\\\").Replace("\"", "\\\"");
 75    }
 76
 77    /// <summary>
 78    /// Escapes a string for use in a verbatim C# string literal.
 79    /// </summary>
 80    public static string EscapeVerbatimStringLiteral(string value)
 81    {
 47582        if (string.IsNullOrEmpty(value))
 083            return string.Empty;
 84        // In verbatim strings, only double-quotes need escaping (by doubling them)
 47585        return value.Replace("\"", "\"\"");
 86    }
 87
 88    /// <summary>
 89    /// Escapes content for use in XML documentation.
 90    /// </summary>
 91    public static string EscapeXmlContent(string content)
 92    {
 93        // The content from GetDocumentationCommentXml() is already parsed,
 94        // so entities like &lt; are already decoded. We need to re-encode them.
 795        return content
 796            .Replace("&", "&amp;")
 797            .Replace("<", "&lt;")
 798            .Replace(">", "&gt;")
 799            .Replace("\"", "&quot;")
 7100            .Replace("'", "&apos;");
 101    }
 102
 103    /// <summary>
 104    /// Gets the simple type name from a fully qualified name.
 105    /// "global::System.String" -> "String"
 106    /// </summary>
 107    public static string GetSimpleTypeName(string fullyQualifiedName)
 108    {
 159109        var parts = fullyQualifiedName.Split('.');
 159110        return parts[parts.Length - 1];
 111    }
 112
 113    /// <summary>
 114    /// Converts a name to camelCase, removing leading 'I' for interfaces.
 115    /// </summary>
 116    public static string ToCamelCase(string name)
 117    {
 167118        if (string.IsNullOrEmpty(name))
 0119            return name;
 120
 121        // Remove leading 'I' for interfaces
 167122        if (name.Length > 1 && name[0] == 'I' && char.IsUpper(name[1]))
 123123            name = name.Substring(1);
 124
 167125        return char.ToLowerInvariant(name[0]) + name.Substring(1);
 126    }
 127
 128    /// <summary>
 129    /// Strips the "global::" prefix from a type name if present.
 130    /// </summary>
 131    public static string StripGlobalPrefix(string name)
 132    {
 18501133        return name.StartsWith("global::", StringComparison.Ordinal)
 18501134            ? name.Substring(8)
 18501135            : name;
 136    }
 137
 138    /// <summary>
 139    /// Gets the short type name from a fully qualified name.
 140    /// Removes global:: prefix and namespace.
 141    /// </summary>
 142    public static string GetShortTypeName(string fullyQualifiedTypeName)
 143    {
 10285966144        var name = fullyQualifiedTypeName;
 10285966145        if (name.StartsWith("global::", StringComparison.Ordinal))
 10209334146            name = name.Substring(8);
 147
 148        // For generic types, find the last dot before the generic type parameter
 149        // e.g., "Microsoft.Extensions.Options.IOptions<Foo.Bar>" -> find dot before "IOptions"
 10285966150        var genericStart = name.IndexOf('<');
 151
 10285966152        if (genericStart < 0)
 153        {
 154            // Non-generic type: just find the last dot
 10280218155            var lastDot = name.LastIndexOf('.');
 10280218156            return lastDot >= 0 ? name.Substring(lastDot + 1) : name;
 157        }
 158
 159        // Generic type: shorten the outer type and recursively shorten type arguments
 5748160        var lastDot2 = name.LastIndexOf('.', genericStart - 1);
 5748161        var outerType = lastDot2 >= 0 ? name.Substring(lastDot2 + 1, genericStart - lastDot2 - 1) : name.Substring(0, ge
 162
 163        // Extract and shorten the type arguments
 5748164        var genericEnd = name.LastIndexOf('>');
 5748165        if (genericEnd > genericStart)
 166        {
 5748167            var typeArgsStr = name.Substring(genericStart + 1, genericEnd - genericStart - 1);
 5748168            var shortenedArgs = ShortenGenericTypeArgs(typeArgsStr);
 5748169            return $"{outerType}<{shortenedArgs}>";
 170        }
 171
 0172        return outerType;
 173    }
 174
 175    private static string ShortenGenericTypeArgs(string typeArgsStr)
 176    {
 177        // Handle multiple type arguments and nested generics
 5748178        var result = new System.Text.StringBuilder();
 5748179        var depth = 0;
 5748180        var start = 0;
 181
 367658182        for (int i = 0; i < typeArgsStr.Length; i++)
 183        {
 178081184            var c = typeArgsStr[i];
 178985185            if (c == '<') depth++;
 178081186            else if (c == '>') depth--;
 176273187            else if (c == ',' && depth == 0)
 188            {
 189                // Found a top-level comma, process this argument
 1216190                var arg = typeArgsStr.Substring(start, i - start).Trim();
 1218191                if (result.Length > 0) result.Append(", ");
 1216192                result.Append(GetShortTypeName(arg));
 1216193                start = i + 1;
 194            }
 195        }
 196
 197        // Process the last (or only) argument
 5748198        var lastArg = typeArgsStr.Substring(start).Trim();
 6962199        if (result.Length > 0) result.Append(", ");
 5748200        result.Append(GetShortTypeName(lastArg));
 201
 5748202        return result.ToString();
 203    }
 204
 205    /// <summary>
 206    /// Gets the proxy type name for an intercepted service.
 207    /// </summary>
 208    public static string GetProxyTypeName(string fullyQualifiedTypeName)
 209    {
 28210        var shortName = GetShortTypeName(fullyQualifiedTypeName);
 28211        return $"{shortName}_InterceptorProxy";
 212    }
 213
 214    /// <summary>
 215    /// Extracts the generic type argument from a generic type name.
 216    /// E.g., "Task&lt;string&gt;" -> "string"
 217    /// </summary>
 218    public static string ExtractGenericTypeArgument(string genericTypeName)
 219    {
 1220        var openBracket = genericTypeName.IndexOf('<');
 1221        var closeBracket = genericTypeName.LastIndexOf('>');
 1222        if (openBracket >= 0 && closeBracket > openBracket)
 223        {
 1224            return genericTypeName.Substring(openBracket + 1, closeBracket - openBracket - 1);
 225        }
 0226        return "object";
 227    }
 228
 229    /// <summary>
 230    /// Gets the base name of a generic type (without type arguments).
 231    /// E.g., "IHandler&lt;Order&gt;" -> "IHandler"
 232    /// </summary>
 233    public static string GetGenericBaseName(string typeName)
 234    {
 21235        var angleBracketIndex = typeName.IndexOf('<');
 21236        return angleBracketIndex >= 0 ? typeName.Substring(0, angleBracketIndex) : typeName;
 237    }
 238
 239    /// <summary>
 240    /// Creates a closed generic type name from an open generic decorator and a closed interface.
 241    /// For example: LoggingDecorator{T} + IHandler{Order} = LoggingDecorator{Order}
 242    /// </summary>
 243    public static string CreateClosedGenericType(string openDecoratorTypeName, string closedInterfaceName, string openIn
 244    {
 7245        var closedArgs = ExtractGenericArguments(closedInterfaceName);
 7246        var openDecoratorBaseName = GetGenericBaseName(openDecoratorTypeName);
 247
 7248        if (closedArgs.Length == 0)
 0249            return openDecoratorTypeName;
 250
 7251        return $"{openDecoratorBaseName}<{string.Join(", ", closedArgs)}>";
 252    }
 253
 254    /// <summary>
 255    /// Extracts the generic type arguments from a closed generic type name.
 256    /// For example: "IHandler{Order, Payment}" returns ["Order", "Payment"]
 257    /// </summary>
 258    public static string[] ExtractGenericArguments(string typeName)
 259    {
 7260        var angleBracketIndex = typeName.IndexOf('<');
 7261        if (angleBracketIndex < 0)
 0262            return Array.Empty<string>();
 263
 7264        var argsStart = angleBracketIndex + 1;
 7265        var argsEnd = typeName.LastIndexOf('>');
 7266        if (argsEnd <= argsStart)
 0267            return Array.Empty<string>();
 268
 7269        var argsString = typeName.Substring(argsStart, argsEnd - argsStart);
 270
 271        // Handle nested generics by parsing with bracket depth tracking
 7272        var args = new List<string>();
 7273        var depth = 0;
 7274        var start = 0;
 275
 474276        for (int i = 0; i < argsString.Length; i++)
 277        {
 230278            var c = argsString[i];
 230279            if (c == '<') depth++;
 230280            else if (c == '>') depth--;
 230281            else if (c == ',' && depth == 0)
 282            {
 1283                args.Add(argsString.Substring(start, i - start).Trim());
 1284                start = i + 1;
 285            }
 286        }
 287
 288        // Add the last argument
 7289        if (start < argsString.Length)
 7290            args.Add(argsString.Substring(start).Trim());
 291
 7292        return args.ToArray();
 293    }
 294
 295    /// <summary>
 296    /// Converts a type name to a valid Mermaid node ID.
 297    /// </summary>
 298    public static string GetMermaidNodeId(string typeName)
 299    {
 58378300        return SanitizeMermaidId(GetShortTypeName(typeName));
 301    }
 302
 303    /// <summary>
 304    /// Converts arbitrary text into a valid Mermaid identifier by replacing every character
 305    /// that is not an ASCII letter, digit, or underscore.
 306    /// </summary>
 307    public static string SanitizeMermaidId(string value)
 308    {
 58540309        if (string.IsNullOrEmpty(value))
 0310            return "_";
 311
 58540312        var sb = new StringBuilder(value.Length);
 2086442313        foreach (var c in value)
 314        {
 984681315            var isSafe = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
 984681316            sb.Append(isSafe ? c : '_');
 317        }
 318
 58540319        return sb.ToString();
 320    }
 321
 322    /// <summary>
 323    /// Escapes text used inside a quoted Mermaid node or subgraph label.
 324    /// </summary>
 325    public static string EscapeMermaidLabel(string label)
 326    {
 162327        return label
 162328            .Replace("\"", "#quot;")
 162329            .Replace("\r", " ")
 162330            .Replace("\n", " ");
 331    }
 332
 333    /// <summary>
 334    /// Escapes text used inside a Markdown pipe table cell so the table structure is preserved.
 335    /// </summary>
 336    public static string EscapeMarkdownTableCell(string value)
 337    {
 8338        return value
 8339            .Replace("|", "\\|")
 8340            .Replace("\r", " ")
 8341            .Replace("\n", " ");
 342    }
 343
 344    /// <summary>
 345    /// Calculates a percentage, handling division by zero.
 346    /// </summary>
 347    public static int Percentage(int count, int total)
 348    {
 339349        if (total == 0) return 0;
 339350        return (int)Math.Round(100.0 * count / total);
 351    }
 352
 353    /// <summary>
 354    /// Extracts the namespace portion from a fully-qualified type name.
 355    /// Strips the leading <c>global::</c> prefix if present.
 356    /// </summary>
 357    public static string GetNamespaceFromTypeName(string fullyQualifiedName)
 358    {
 12359        var name = fullyQualifiedName;
 12360        if (name.StartsWith("global::"))
 361        {
 12362            name = name.Substring(8);
 363        }
 364
 12365        var lastDot = name.LastIndexOf('.');
 12366        return lastDot >= 0 ? name.Substring(0, lastDot) : string.Empty;
 367    }
 368}