Files
Dan Moseley 042a9d2b2c Reorganize plugins into domain-specific groupings (#274)
* Reorganize plugins per agreed structure (#271)

Split the dotnet plugin into domain-specific plugins per discussion
in #271:

- dotnet: narrowed to common everyday C#/.NET coding tasks
- dotnet-diag: perf investigations, debugging, incident analysis
- dotnet-data: data access and Entity Framework
- dotnet-aspnet, dotnet-wpf, dotnet-winforms, dotnet-maui: scaffolded
  empty to convey intent for future framework-specific skills

Moved skills, tests, and CODEOWNERS entries accordingly.
No file content was changed; all skill/test moves are pure renames.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Improve dotnet-diag and dotnet-winforms descriptions

- dotnet-diag: add '.NET' qualifier to clarify scope
- dotnet-winforms: use official 'Windows Forms (WinForms)' name

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add .NET qualifier to dotnet-data description

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Remove empty plugins; list them in CONTRIBUTING.md as future options

Remove the four empty placeholder plugins (dotnet-aspnet, dotnet-wpf,
dotnet-winforms, dotnet-maui) that had only plugin.json and no skills.

Add a 'Plugin organization' section to CONTRIBUTING.md that describes the
domain-specific plugin groupings introduced in this PR and lists the four
removed plugin names as reserved candidates for future contributions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Create dotnet-upgrade plugin with migration/upgrade skills

Move migrate-nullable-references, thread-abort-migration, and
dotnet-aot-compat from dotnet to the new dotnet-upgrade plugin.

Update CODEOWNERS paths (same owners), CONTRIBUTING.md plugin table,
and fix the thread-abort-migration SKILL.md link in CONTRIBUTING.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-06 21:01:52 -08:00

241 lines
9.0 KiB
C#

using System.Text.RegularExpressions;
namespace Textualizer;
/// <summary>
/// Provides extension methods for converting strings between human-readable form
/// and PascalCase/camelCase representations.
/// </summary>
public static partial class StringReadabilityExtensions
{
#if NET7_0_OR_GREATER
[GeneratedRegex(@"[\p{Lu}]+(?=\p{Lu}[\p{Ll}])|[\p{Lu}]?[\p{Ll}]+|\d+")]
private static partial Regex PascalCaseWordPartsRegexGenerated();
[GeneratedRegex(@"^[\p{Zs}\t]+$")]
private static partial Regex FreestandingSpacingCharRegexGenerated();
private static Regex PascalCaseWordPartsRegex() => PascalCaseWordPartsRegexGenerated();
private static Regex FreestandingSpacingCharRegex() => FreestandingSpacingCharRegexGenerated();
#else
private static readonly Regex PascalCaseWordPartsRegexField =
new(@"[\p{Lu}]+(?=\p{Lu}[\p{Ll}])|[\p{Lu}]?[\p{Ll}]+|\d+", RegexOptions.Compiled);
private static readonly Regex FreestandingSpacingCharRegexField =
new(@"^[\p{Zs}\t]+$", RegexOptions.Compiled);
private static Regex PascalCaseWordPartsRegex() => PascalCaseWordPartsRegexField;
private static Regex FreestandingSpacingCharRegex() => FreestandingSpacingCharRegexField;
#endif
/// <summary>
/// Converts a machine-readable string (PascalCase, camelCase, snake_case, kebab-case)
/// to a human-readable form with spaces between words.
/// </summary>
/// <param name="input">The string to humanize.</param>
/// <returns>A human-readable version of the input string.</returns>
/// <example>
/// <code>
/// "PascalCaseInputStringIsTurnedIntoSentence".Humanize()
/// => "Pascal case input string is turned into sentence"
///
/// "Underscored_input_String_is_turned_into_sentence".Humanize()
/// => "Underscored input string is turned into sentence"
///
/// "HTMLParser".Humanize() => "HTML parser"
/// </code>
/// </example>
public static string Humanize(this string input)
{
if (string.IsNullOrWhiteSpace(input))
{
return input;
}
// Handle already-spaced strings
if (FreestandingSpacingCharRegex().IsMatch(input))
{
return input;
}
// Handle underscore/dash separated
if (input.Contains('_') || input.Contains('-'))
{
return FromUnderscoreDashSeparatedWords(input);
}
return FromPascalCase(input);
}
/// <summary>
/// Converts a string from underscore or dash separation to a human-readable form.
/// </summary>
/// <param name="input">The underscore or dash separated string.</param>
/// <returns>A space-separated human-readable string.</returns>
internal static string FromUnderscoreDashSeparatedWords(string input) =>
string.Join(" ", input.Split(['_', '-']));
/// <summary>
/// Converts a PascalCase or camelCase string to a human-readable form.
/// Preserves acronyms (consecutive uppercase letters) as-is.
/// </summary>
/// <param name="input">The PascalCase string to convert.</param>
/// <returns>A space-separated human-readable string.</returns>
internal static string FromPascalCase(string input)
{
// Anti-pattern: LINQ chain .Cast<Match>().Select() — allocates enumerator + delegate
var result = string.Join(" ", PascalCaseWordPartsRegex()
.Matches(input)
.Cast<Match>()
.Select(match =>
{
var value = match.Value;
// Anti-pattern: .All(char.IsUpper) — LINQ on string allocates enumerator
return value.All(char.IsUpper) &&
(value.Length > 1 || (match.Index > 0 && input[match.Index - 1] == ' ') || value == "I")
? value
// Anti-pattern: .ToLower() without culture inside lambda — per-match allocation
: value.ToLower();
}));
// Anti-pattern: .All() again and .Contains without StringComparison
if (result.All(c => c == ' ' || char.IsUpper(c)) && result.Contains(' '))
{
result = result.ToLower();
}
return result.Length > 0 ? char.ToUpper(result[0]) + result[1..] : result;
}
/// <summary>
/// Concatenates two <see cref="ReadOnlySpan{T}"/> of <see cref="char"/> into a new string.
/// Uses efficient span-based concatenation to avoid intermediate allocations.
/// </summary>
/// <param name="first">The first span of characters.</param>
/// <param name="second">The second span of characters.</param>
/// <returns>A new string containing the concatenated characters.</returns>
internal static string Concat(ReadOnlySpan<char> first, ReadOnlySpan<char> second)
{
// Correct: uses Span-based concatenation — positive finding
return string.Concat(first, second);
}
/// <summary>
/// Concatenates a single character with a <see cref="ReadOnlySpan{T}"/> of characters.
/// </summary>
/// <param name="c">The character to prepend.</param>
/// <param name="rest">The remaining characters.</param>
/// <returns>A new string with the character prepended.</returns>
internal static string Concat(char c, ReadOnlySpan<char> rest)
{
Span<char> buffer = stackalloc char[1 + rest.Length];
buffer[0] = c;
rest.CopyTo(buffer[1..]);
return new string(buffer);
}
}
/// <summary>
/// Provides extension methods for converting human-readable strings back to PascalCase.
/// </summary>
public static class StringDehumanizeExtensions
{
/// <summary>
/// Converts a humanized string back to PascalCase by capitalizing each word and removing spaces.
/// </summary>
/// <param name="input">The humanized string to dehumanize.</param>
/// <returns>
/// A PascalCase string. If the input is already PascalCase (no spaces), it is returned unchanged.
/// </returns>
/// <example>
/// <code>
/// "some string".Dehumanize() => "SomeString"
/// "Some String".Dehumanize() => "SomeString"
/// "SomeStringAndAnotherString".Dehumanize() => "SomeStringAndAnotherString"
/// </code>
/// </example>
public static string Dehumanize(this string input)
{
var words = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (words.Length == 0)
{
return input;
}
if (words.Length == 1)
{
return words[0].Humanize().Pascalize();
}
// Anti-pattern: string.Concat(words.Select(...)) — LINQ allocates enumerator + delegate
// Each word goes through Humanize() then Pascalize() — double processing
return string.Concat(words.Select(word => word.Humanize().Pascalize()));
}
/// <summary>
/// Converts a string to PascalCase by capitalizing the first letter of each word
/// and removing separators.
/// </summary>
/// <param name="input">The string to pascalize.</param>
/// <returns>A PascalCase representation of the input.</returns>
public static string Pascalize(this string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
return input.Humanize().Transform(To.TitleCase).Replace(" ", "");
}
}
/// <summary>
/// Provides string transformation capabilities using the strategy pattern.
/// </summary>
public interface IStringTransformer
{
/// <summary>Transforms the input string.</summary>
string Transform(string input);
}
/// <summary>
/// A portal to string transformation using IStringTransformer.
/// </summary>
public static class To
{
/// <summary>
/// Transforms a string using the provided transformers, applied in order.
/// </summary>
/// <param name="input">The string to transform.</param>
/// <param name="transformers">One or more transformers to apply sequentially.</param>
/// <returns>The transformed string.</returns>
// Anti-pattern: params allocates a T[] on every call, even for the common 1-arg case
// Should add 1-arg and 2-arg overloads to avoid the array allocation
public static string Transform(this string input, params IStringTransformer[] transformers) =>
transformers.Aggregate(input, (current, t) => t.Transform(current));
/// <summary>Changes string to title case.</summary>
public static IStringTransformer TitleCase { get; } = new ToTitleCase();
/// <summary>Changes the string to lower case.</summary>
public static IStringTransformer LowerCase { get; } = new ToLowerCase();
/// <summary>Changes the string to upper case.</summary>
public static IStringTransformer UpperCase { get; } = new ToUpperCase();
}
internal class ToTitleCase : IStringTransformer
{
public string Transform(string input) =>
System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input);
}
internal class ToLowerCase : IStringTransformer
{
public string Transform(string input) => input.ToLowerInvariant();
}
internal class ToUpperCase : IStringTransformer
{
public string Transform(string input) => input.ToUpperInvariant();
}