< Summary

Information
Class: NexusLabs.Needlr.Generators.GeneratorHelpers
Assembly: NexusLabs.Needlr.Generators
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.Generators/GeneratorHelpers.cs
Line coverage
83%
Covered lines: 96
Uncovered lines: 19
Coverable lines: 115
Total lines: 319
Line coverage: 83.4%
Branch coverage
75%
Covered branches: 69
Total branches: 92
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/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
 9namespace NexusLabs.Needlr.Generators;
 10
 11/// <summary>
 12/// Utility methods for source code generation.
 13/// </summary>
 14internal static class GeneratorHelpers
 15{
 16    /// <summary>
 17    /// Sanitizes an assembly name to be a valid C# identifier for use in namespaces.
 18    /// </summary>
 19    public static string SanitizeIdentifier(string name)
 20    {
 7521821        if (string.IsNullOrEmpty(name))
 022            return "Generated";
 23
 7521824        var sb = new StringBuilder(name.Length);
 381534825        foreach (var c in name)
 26        {
 183245627            if (char.IsLetterOrDigit(c) || c == '_')
 28            {
 168638729                sb.Append(c);
 30            }
 14606931            else if (c == '.' || c == '-' || c == ' ')
 32            {
 33                // Keep dots for namespace segments, replace dashes/spaces with underscores
 14606934                sb.Append(c == '.' ? '.' : '_');
 35            }
 36            // Skip other characters
 37        }
 38
 7521839        var result = sb.ToString();
 40
 41        // Ensure each segment doesn't start with a digit
 7521842        var segments = result.Split('.');
 59301043        for (int i = 0; i < segments.Length; i++)
 44        {
 22128745            if (segments[i].Length > 0 && char.IsDigit(segments[i][0]))
 46            {
 047                segments[i] = "_" + segments[i];
 48            }
 49        }
 50
 29650551        return string.Join(".", segments.Where(s => s.Length > 0));
 52    }
 53
 54    /// <summary>
 55    /// Escapes a string for use in a regular C# string literal.
 56    /// </summary>
 57    public static string EscapeStringLiteral(string value)
 58    {
 80036759        if (string.IsNullOrEmpty(value))
 108260            return string.Empty;
 79928561        return value.Replace("\\", "\\\\").Replace("\"", "\\\"");
 62    }
 63
 64    /// <summary>
 65    /// Escapes a string for use in a verbatim C# string literal.
 66    /// </summary>
 67    public static string EscapeVerbatimStringLiteral(string value)
 68    {
 47569        if (string.IsNullOrEmpty(value))
 070            return string.Empty;
 71        // In verbatim strings, only double-quotes need escaping (by doubling them)
 47572        return value.Replace("\"", "\"\"");
 73    }
 74
 75    /// <summary>
 76    /// Escapes content for use in XML documentation.
 77    /// </summary>
 78    public static string EscapeXmlContent(string content)
 79    {
 80        // The content from GetDocumentationCommentXml() is already parsed,
 81        // so entities like &lt; are already decoded. We need to re-encode them.
 782        return content
 783            .Replace("&", "&amp;")
 784            .Replace("<", "&lt;")
 785            .Replace(">", "&gt;")
 786            .Replace("\"", "&quot;")
 787            .Replace("'", "&apos;");
 88    }
 89
 90    /// <summary>
 91    /// Gets the simple type name from a fully qualified name.
 92    /// "global::System.String" -> "String"
 93    /// </summary>
 94    public static string GetSimpleTypeName(string fullyQualifiedName)
 95    {
 13296        var parts = fullyQualifiedName.Split('.');
 13297        return parts[parts.Length - 1];
 98    }
 99
 100    /// <summary>
 101    /// Converts a name to camelCase, removing leading 'I' for interfaces.
 102    /// </summary>
 103    public static string ToCamelCase(string name)
 104    {
 142105        if (string.IsNullOrEmpty(name))
 0106            return name;
 107
 108        // Remove leading 'I' for interfaces
 142109        if (name.Length > 1 && name[0] == 'I' && char.IsUpper(name[1]))
 98110            name = name.Substring(1);
 111
 142112        return char.ToLowerInvariant(name[0]) + name.Substring(1);
 113    }
 114
 115    /// <summary>
 116    /// Strips the "global::" prefix from a type name if present.
 117    /// </summary>
 118    public static string StripGlobalPrefix(string name)
 119    {
 18300120        return name.StartsWith("global::", StringComparison.Ordinal)
 18300121            ? name.Substring(8)
 18300122            : name;
 123    }
 124
 125    /// <summary>
 126    /// Gets the short type name from a fully qualified name.
 127    /// Removes global:: prefix and namespace.
 128    /// </summary>
 129    public static string GetShortTypeName(string fullyQualifiedTypeName)
 130    {
 6098995131        var name = fullyQualifiedTypeName;
 6098995132        if (name.StartsWith("global::", StringComparison.Ordinal))
 6033338133            name = name.Substring(8);
 134
 135        // For generic types, find the last dot before the generic type parameter
 136        // e.g., "Microsoft.Extensions.Options.IOptions<Foo.Bar>" -> find dot before "IOptions"
 6098995137        var genericStart = name.IndexOf('<');
 138
 6098995139        if (genericStart < 0)
 140        {
 141            // Non-generic type: just find the last dot
 6095668142            var lastDot = name.LastIndexOf('.');
 6095668143            return lastDot >= 0 ? name.Substring(lastDot + 1) : name;
 144        }
 145
 146        // Generic type: shorten the outer type and recursively shorten type arguments
 3327147        var lastDot2 = name.LastIndexOf('.', genericStart - 1);
 3327148        var outerType = lastDot2 >= 0 ? name.Substring(lastDot2 + 1, genericStart - lastDot2 - 1) : name.Substring(0, ge
 149
 150        // Extract and shorten the type arguments
 3327151        var genericEnd = name.LastIndexOf('>');
 3327152        if (genericEnd > genericStart)
 153        {
 3327154            var typeArgsStr = name.Substring(genericStart + 1, genericEnd - genericStart - 1);
 3327155            var shortenedArgs = ShortenGenericTypeArgs(typeArgsStr);
 3327156            return $"{outerType}<{shortenedArgs}>";
 157        }
 158
 0159        return outerType;
 160    }
 161
 162    private static string ShortenGenericTypeArgs(string typeArgsStr)
 163    {
 164        // Handle multiple type arguments and nested generics
 3327165        var result = new System.Text.StringBuilder();
 3327166        var depth = 0;
 3327167        var start = 0;
 168
 213398169        for (int i = 0; i < typeArgsStr.Length; i++)
 170        {
 103372171            var c = typeArgsStr[i];
 103976172            if (c == '<') depth++;
 103372173            else if (c == '>') depth--;
 102164174            else if (c == ',' && depth == 0)
 175            {
 176                // Found a top-level comma, process this argument
 609177                var arg = typeArgsStr.Substring(start, i - start).Trim();
 611178                if (result.Length > 0) result.Append(", ");
 609179                result.Append(GetShortTypeName(arg));
 609180                start = i + 1;
 181            }
 182        }
 183
 184        // Process the last (or only) argument
 3327185        var lastArg = typeArgsStr.Substring(start).Trim();
 3934186        if (result.Length > 0) result.Append(", ");
 3327187        result.Append(GetShortTypeName(lastArg));
 188
 3327189        return result.ToString();
 190    }
 191
 192    /// <summary>
 193    /// Gets the proxy type name for an intercepted service.
 194    /// </summary>
 195    public static string GetProxyTypeName(string fullyQualifiedTypeName)
 196    {
 30197        var shortName = GetShortTypeName(fullyQualifiedTypeName);
 30198        return $"{shortName}_InterceptorProxy";
 199    }
 200
 201    /// <summary>
 202    /// Gets the fully qualified validator class name for an options type.
 203    /// E.g., "global::TestApp.StripeOptions" -> "global::TestApp.Generated.StripeOptionsValidator"
 204    /// </summary>
 205    public static string GetValidatorClassName(string optionsTypeName)
 206    {
 0207        var shortName = GetShortTypeName(optionsTypeName);
 208
 0209        var name = optionsTypeName;
 0210        if (name.StartsWith("global::", StringComparison.Ordinal))
 0211            name = name.Substring(8);
 212
 0213        var lastDot = name.LastIndexOf('.');
 0214        var ns = lastDot >= 0 ? name.Substring(0, lastDot) : "";
 215
 0216        var validatorName = shortName + "Validator";
 0217        return string.IsNullOrEmpty(ns)
 0218            ? $"global::{validatorName}"
 0219            : $"global::{ns}.Generated.{validatorName}";
 220    }
 221
 222    /// <summary>
 223    /// Extracts the generic type argument from a generic type name.
 224    /// E.g., "Task&lt;string&gt;" -> "string"
 225    /// </summary>
 226    public static string ExtractGenericTypeArgument(string genericTypeName)
 227    {
 1228        var openBracket = genericTypeName.IndexOf('<');
 1229        var closeBracket = genericTypeName.LastIndexOf('>');
 1230        if (openBracket >= 0 && closeBracket > openBracket)
 231        {
 1232            return genericTypeName.Substring(openBracket + 1, closeBracket - openBracket - 1);
 233        }
 0234        return "object";
 235    }
 236
 237    /// <summary>
 238    /// Gets the base name of a generic type (without type arguments).
 239    /// E.g., "IHandler&lt;Order&gt;" -> "IHandler"
 240    /// </summary>
 241    public static string GetGenericBaseName(string typeName)
 242    {
 21243        var angleBracketIndex = typeName.IndexOf('<');
 21244        return angleBracketIndex >= 0 ? typeName.Substring(0, angleBracketIndex) : typeName;
 245    }
 246
 247    /// <summary>
 248    /// Creates a closed generic type name from an open generic decorator and a closed interface.
 249    /// For example: LoggingDecorator{T} + IHandler{Order} = LoggingDecorator{Order}
 250    /// </summary>
 251    public static string CreateClosedGenericType(string openDecoratorTypeName, string closedInterfaceName, string openIn
 252    {
 7253        var closedArgs = ExtractGenericArguments(closedInterfaceName);
 7254        var openDecoratorBaseName = GetGenericBaseName(openDecoratorTypeName);
 255
 7256        if (closedArgs.Length == 0)
 0257            return openDecoratorTypeName;
 258
 7259        return $"{openDecoratorBaseName}<{string.Join(", ", closedArgs)}>";
 260    }
 261
 262    /// <summary>
 263    /// Extracts the generic type arguments from a closed generic type name.
 264    /// For example: "IHandler{Order, Payment}" returns ["Order", "Payment"]
 265    /// </summary>
 266    public static string[] ExtractGenericArguments(string typeName)
 267    {
 7268        var angleBracketIndex = typeName.IndexOf('<');
 7269        if (angleBracketIndex < 0)
 0270            return Array.Empty<string>();
 271
 7272        var argsStart = angleBracketIndex + 1;
 7273        var argsEnd = typeName.LastIndexOf('>');
 7274        if (argsEnd <= argsStart)
 0275            return Array.Empty<string>();
 276
 7277        var argsString = typeName.Substring(argsStart, argsEnd - argsStart);
 278
 279        // Handle nested generics by parsing with bracket depth tracking
 7280        var args = new List<string>();
 7281        var depth = 0;
 7282        var start = 0;
 283
 474284        for (int i = 0; i < argsString.Length; i++)
 285        {
 230286            var c = argsString[i];
 230287            if (c == '<') depth++;
 230288            else if (c == '>') depth--;
 230289            else if (c == ',' && depth == 0)
 290            {
 1291                args.Add(argsString.Substring(start, i - start).Trim());
 1292                start = i + 1;
 293            }
 294        }
 295
 296        // Add the last argument
 7297        if (start < argsString.Length)
 7298            args.Add(argsString.Substring(start).Trim());
 299
 7300        return args.ToArray();
 301    }
 302
 303    /// <summary>
 304    /// Converts a type name to a valid Mermaid node ID.
 305    /// </summary>
 306    public static string GetMermaidNodeId(string typeName)
 307    {
 53235308        return GetShortTypeName(typeName).Replace(".", "_").Replace("<", "_").Replace(">", "_").Replace(",", "_");
 309    }
 310
 311    /// <summary>
 312    /// Calculates a percentage, handling division by zero.
 313    /// </summary>
 314    public static int Percentage(int count, int total)
 315    {
 327316        if (total == 0) return 0;
 327317        return (int)Math.Round(100.0 * count / total);
 318    }
 319}