Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Security.Cryptography;

namespace ANcpLua.Roslyn.Utilities;

#if ANCPLUA_ROSLYN_PUBLIC
public
#else
internal
#endif
static partial class StringExtensions

Check notice on line 10 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs#L10

'partial' is gratuitous in this context.
{
/// <summary>
/// Computes a deterministic 8-character uppercase hexadecimal hash from a string.
/// </summary>
/// <param name="input">The input string to hash.</param>
/// <returns>
/// An 8-character uppercase hexadecimal string derived from the SHA-256 hash of the input.
/// Returns <c>"00000000"</c> if the input is <c>null</c> or empty.
/// </returns>
/// <remarks>
/// <para>
/// Produces a short, deterministic identifier suitable for use as a suffix in generated type
/// names, file hint names, or graph node IDs where full hashes are too long.
/// </para>
/// </remarks>
public static string ToShortHash(this string input)
{
return ToShortHash(input, 8);
}

/// <summary>
/// Computes a deterministic N-character hexadecimal hash from a string.
/// </summary>
/// <param name="input">The input string to hash.</param>
/// <param name="hexChars">Number of hex characters to return (1–64).</param>
/// <param name="lowercase">
/// If <c>true</c>, returns lowercase hex; otherwise uppercase. Defaults to <c>false</c> for visual
/// parity with <see cref="ToShortHash(string)" />.
/// </param>
/// <returns>
/// An <paramref name="hexChars" />-character hexadecimal string derived from the SHA-256 hash of
/// the input. Returns a string of <paramref name="hexChars" /> zeroes if the input is <c>null</c>
/// or empty.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="hexChars" /> is not in the range 1–64.
/// </exception>
/// <remarks>
/// <para>
/// The two overloads share an internal implementation so adding cases doesn't require two
/// parallel branches. Cyclomatic complexity per overload stays at 1–2.
/// </para>
/// </remarks>
public static string ToShortHash(this string input, int hexChars, bool lowercase = false)
{
if (hexChars is < 1 or > 64)

Check notice on line 56 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs#L56

Add curly braces around the nested statement(s) in this 'if' block.
throw new ArgumentOutOfRangeException(nameof(hexChars), hexChars, "Must be 1–64.");

if (string.IsNullOrEmpty(input))

Check notice on line 59 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs#L59

Add curly braces around the nested statement(s) in this 'if' block.
return new string('0', hexChars);

var hex = ComputeSha256Hex(input);
if (lowercase) hex = hex.ToLowerInvariant();

Check notice on line 63 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Hashing.cs#L63

Add curly braces around the nested statement(s) in this 'if' block.
return hex[..hexChars];
}

private static string ComputeSha256Hex(string input)
{
#if NET5_0_OR_GREATER
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(hash);
#else
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
return BitConverter.ToString(hash).Replace("-", "");
#endif
}
}
136 changes: 136 additions & 0 deletions src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
namespace ANcpLua.Roslyn.Utilities;

#if ANCPLUA_ROSLYN_PUBLIC
public
#else
internal
#endif
static partial class StringExtensions

Check notice on line 8 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs#L8

'partial' is gratuitous in this context.
{
// C# reserved keywords. Sourced from the official C# language reference:
// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/
// ToParameterName checks membership via O(1) hash lookup, replacing what
// used to be an 80-arm switch expression (CC ~82) with CC ~4.
private static readonly HashSet<string> s_cSharpKeywords =
[
"abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked",
"class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else",
"enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for",
"foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock",
"long", "namespace", "new", "null", "object", "operator", "out", "override", "params",
"private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", "short",
"sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "true",
"try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual",
"void", "volatile", "while"
];

/// <summary>
/// Converts a string to PascalCase by making the first character uppercase.
/// </summary>
/// <param name="input">The input string to convert.</param>
/// <returns>
/// The input string with its first character converted to uppercase using invariant culture rules.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="input" /> is <c>null</c>.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="input" /> is an empty string.
/// </exception>
/// <remarks>
/// <para>
/// This method is useful for converting field names to property names in source generators.
/// For example, <c>"firstName"</c> becomes <c>"FirstName"</c>.
/// </para>
/// </remarks>
/// <seealso cref="ToParameterName" />
public static string ToPropertyName(this string input)
{
if (input is null)

Check notice on line 49 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs#L49

Add curly braces around the nested statement(s) in this 'if' block.
throw new ArgumentNullException(nameof(input));

if (input.Length == 0)

Check notice on line 52 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs#L52

Add curly braces around the nested statement(s) in this 'if' block.
throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input));

#if NET6_0_OR_GREATER
return string.Concat(input[0].ToString().ToUpper(CultureInfo.InvariantCulture), input.AsSpan(1));
#else
return input[0].ToString().ToUpper(CultureInfo.InvariantCulture) + input[1..];
#endif
}

/// <summary>
/// Converts a string to camelCase by making the first character lowercase,
/// and escapes C# reserved keywords with the <c>@</c> prefix.
/// </summary>
/// <param name="input">The input string to convert.</param>
/// <returns>
/// The input string converted to a valid C# parameter name. If the resulting name
/// is a C# keyword, it is prefixed with <c>@</c> (e.g., <c>"class"</c> becomes <c>"@class"</c>).
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="input" /> is <c>null</c>.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="input" /> is an empty string.
/// </exception>
/// <remarks>
/// <para>
/// Handles every C# reserved keyword via a single hash-set lookup. The keyword table is
/// the source of truth — adding a new keyword is one string instead of a new switch arm.
/// </para>
/// </remarks>
/// <seealso cref="ToPropertyName" />
public static string ToParameterName(this string input)
{
if (input is null)
throw new ArgumentNullException(nameof(input));

if (input.Length == 0)
throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input));

var lowered = input.ToLowerInvariant();
if (s_cSharpKeywords.Contains(lowered))

Check notice on line 93 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs#L93

Add curly braces around the nested statement(s) in this 'if' block.
return "@" + lowered;

#if NET6_0_OR_GREATER
return string.Concat(input[0].ToString().ToLower(CultureInfo.InvariantCulture), input.AsSpan(1));
#else
return input[0].ToString().ToLower(CultureInfo.InvariantCulture) + input[1..];
#endif
}

/// <summary>
/// Sanitizes a string for use as a C# identifier by replacing non-alphanumeric characters with underscores.
/// </summary>
/// <param name="name">The name to sanitize.</param>
/// <returns>A string containing only letters, digits, and underscores.</returns>
public static string SanitizeIdentifier(this string name)
{
if (string.IsNullOrEmpty(name)) return "_";

Check notice on line 110 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs#L110

Add curly braces around the nested statement(s) in this 'if' block.

var sb = new StringBuilder(name.Length);
foreach (var c in name)

Check notice on line 113 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs#L113

Add curly braces around the nested statement(s) in this 'foreach' block.
sb.Append(IsIdentifierChar(c) ? c : '_');

return sb.ToString();
}

private static bool IsIdentifierChar(char c) => char.IsLetterOrDigit(c) || c == '_';

/// <summary>
/// Escapes a string for use as a C# string literal.
/// </summary>
/// <param name="s">The string to escape.</param>
/// <returns>The escaped string, suitable for placement inside double quotes.</returns>
public static string EscapeCSharpString(this string s)
{
if (string.IsNullOrEmpty(s)) return string.Empty;

Check notice on line 128 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Identifiers.cs#L128

Add curly braces around the nested statement(s) in this 'if' block.

return s
.Replace("\\", @"\\")
.Replace("\"", "\\\"")
.Replace("\r", "\\r")
.Replace("\n", "\\n");
}
}
156 changes: 156 additions & 0 deletions src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
namespace ANcpLua.Roslyn.Utilities;

#if ANCPLUA_ROSLYN_PUBLIC
public
#else
internal
#endif
static partial class StringExtensions

Check notice on line 8 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs#L8

'partial' is gratuitous in this context.
{
private const char DoubleQuoteChar = '"';
private const char SingleQuoteChar = '\'';

// ========== Double-quoting ==========

/// <summary>
/// Wraps the string in double quotes if it contains a space (default trigger).
/// </summary>
/// <param name="str">The string to conditionally quote.</param>
/// <returns>The original string if already quoted or no disallowed chars found; otherwise double-quoted.</returns>
public static string DoubleQuoteIfNeeded(this string? str)
{
return str.DoubleQuoteIfNeeded(' ');
}

/// <summary>
/// Wraps the string in double quotes if it contains any of the specified disallowed characters.
/// </summary>
/// <param name="str">The string to conditionally quote.</param>
/// <param name="disallowed">Characters that trigger quoting.</param>
/// <returns>The original string if already quoted or no disallowed chars found; otherwise double-quoted.</returns>
public static string DoubleQuoteIfNeeded(this string? str, params char[] disallowed)
{
return QuoteIfNeeded(str, DoubleQuoteChar, disallowed);
}

/// <summary>
/// Wraps the string in double quotes, escaping any existing double quotes.
/// </summary>
/// <param name="str">The string to quote.</param>
/// <returns>The double-quoted string.</returns>
public static string DoubleQuote(this string? str)
{
return Quote(str, DoubleQuoteChar);
}

// ========== Single-quoting ==========

/// <summary>
/// Wraps the string in single quotes if it contains a space (default trigger).
/// </summary>
/// <param name="str">The string to conditionally quote.</param>
/// <returns>The original string if already quoted or no disallowed chars found; otherwise single-quoted.</returns>
public static string SingleQuoteIfNeeded(this string? str)
{
return str.SingleQuoteIfNeeded(' ');
}

/// <summary>
/// Wraps the string in single quotes if it contains any of the specified disallowed characters.
/// </summary>
/// <param name="str">The string to conditionally quote.</param>
/// <param name="disallowed">Characters that trigger quoting.</param>
/// <returns>The original string if already quoted or no disallowed chars found; otherwise single-quoted.</returns>
public static string SingleQuoteIfNeeded(this string? str, params char[] disallowed)
{
return QuoteIfNeeded(str, SingleQuoteChar, disallowed);
}

/// <summary>
/// Wraps the string in single quotes, escaping any existing single quotes.
/// </summary>
/// <param name="str">The string to quote.</param>
/// <returns>The single-quoted string.</returns>
public static string SingleQuote(this string? str)
{
return Quote(str, SingleQuoteChar);
}

/// <summary>
/// Checks whether the string is wrapped in double quotes.
/// </summary>
/// <param name="str">The string to check.</param>
/// <returns><c>true</c> if the string starts and ends with a double quote.</returns>
public static bool IsDoubleQuoted([NotNullWhen(true)] this string? str)
{
return IsQuoted(str, DoubleQuoteChar);
}

/// <summary>
/// Checks whether the string is wrapped in single quotes.
/// </summary>
/// <param name="str">The string to check.</param>
/// <returns><c>true</c> if the string starts and ends with a single quote.</returns>
public static bool IsSingleQuoted([NotNullWhen(true)] this string? str)
{
return IsQuoted(str, SingleQuoteChar);
}

// ========== Graph label escaping ==========

/// <summary>
/// Escapes a string for use as a label in Graphviz DOT format.
/// </summary>
/// <param name="label">The label text to escape.</param>
/// <returns>The escaped label safe for use inside DOT double-quoted strings.</returns>
/// <remarks>
/// Escapes backslashes, double quotes, and newlines which are special characters in DOT labels.
/// </remarks>
public static string EscapeDotLabel(this string label)
{
return label
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\n", "\\n");
}

/// <summary>
/// Escapes a string for use as a label in Mermaid diagram format.
/// </summary>
/// <param name="label">The label text to escape.</param>
/// <returns>The escaped label safe for use in Mermaid node and edge labels.</returns>
/// <remarks>
/// Encodes double quotes as HTML entities and newlines as HTML line breaks,
/// which Mermaid renderers interpret correctly.
/// </remarks>
public static string EscapeMermaidLabel(this string label)
{
return label
.Replace("\"", "&quot;")
.Replace("\n", "<br/>");
}

// ========== Private quote primitives ==========

private static string QuoteIfNeeded(string? str, char quote, params char[] disallowed)
{
if (string.IsNullOrWhiteSpace(str))

Check notice on line 137 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs#L137

Add curly braces around the nested statement(s) in this 'if' block.
return string.Empty;

if (IsQuoted(str, quote) || str!.AsSpan().IndexOfAny(disallowed) < 0)

Check notice on line 140 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs#L140

Add curly braces around the nested statement(s) in this 'if' block.
return str!;

return Quote(str, quote);
}

private static string Quote(string? str, char quote)

Check notice on line 146 in src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/ANcpLua.Roslyn.Utilities/StringExtensions.Quoting.cs#L146

Rename the parameter 'quote' so that it does not duplicate the method name.
{
var escaped = str?.Replace(quote.ToString(), "\\" + quote);
return string.Concat(quote, escaped, quote);
}

private static bool IsQuoted([NotNullWhen(true)] string? str, char quote)
{
return str is [var first, .., var last] && first == quote && last == quote;
}
}
Loading
Loading