Skip to content

Delegate naming-style spelling to ktsu.CaseConverter #71

Description

@matt-edmondson

What's hand-rolled

NameStyles.Spell takes a space-separated run of words and spells it as Pascal, Camel, or Snake case, by upper/lower-casing the first character of each word with char.ToUpper/char.ToLower:

internal static class NameStyles
{
/// <summary>
/// Spells a name made of space-separated words.
/// </summary>
/// <param name="words">The name, as words separated by spaces.</param>
/// <param name="style">How the target spells a member's name.</param>
/// <returns>The name.</returns>
/// <remarks>
/// The words come in separated because the caller knows where they are and this cannot: a name
/// already written as <c>setValue</c> or <c>set_value</c> would have to be taken apart first,
/// and every rule for doing that is wrong about something.
/// </remarks>
internal static string Spell(string words, NamingStyle style)
{
string[] parts = [.. words.Split(' ').Where(part => part.Length > 0)];
if (parts.Length == 0)
{
return string.Empty;
}
return style switch
{
NamingStyle.Snake => string.Join("_", parts.Select(part => part.ToLowerInvariant())),
NamingStyle.Camel => Lower(parts[0]) + string.Concat(parts.Skip(1).Select(Upper)),
_ => string.Concat(parts.Select(Upper)),
};
}
private static string Upper(string word) =>
char.ToUpper(word[0], CultureInfo.InvariantCulture) + word[1..];
private static string Lower(string word) =>
char.ToLower(word[0], CultureInfo.InvariantCulture) + word[1..];
}

It has one call site, PropertyDeclaration.GetterName/SetterName, which builds an invented accessor name (e.g. "set " + Name) and spells it in the target's naming convention:

public string GetterName(NamingStyle style) => NameStyles.Spell(Name ?? "value", style);

public string SetterName(NamingStyle style) => NameStyles.Spell($"set {Name ?? "value"}", style);

What ktsu.CaseConverter provides

ktsu.CaseConverter ships the same three conversions (plus kebab/macro) as string extension methods, and — because its regex-based word splitter treats spaces as word boundaries the same way it treats case changes — they produce identical output when called on an already space-separated string:

"set value".ToPascalCase()"SetValue", .ToCamelCase()"setValue", .ToSnakeCase()"set_value" — the same three outputs NameStyles.Spell produces for NamingStyle.Pascal/Camel/Snake given the same input.

Why it's worth it

Mostly a line-count win for the call sites — Spell is short and currently correct for the ASCII identifiers this AST generates. The one place CaseConverter gets more right: it title-cases through CultureInfo.InvariantCulture.TextInfo.ToTitleCase and matches word/case boundaries against \p{L} (any Unicode letter), where Spell's char.ToUpper(word[0], ...) only touches the first UTF-16 code unit of each word — a word beginning with a character outside the Basic Multilingual Plane (a supplementary-plane letter, e.g. some CJK-B or historic-script identifiers) would have only half its leading surrogate pair touched. Property names sourced from arbitrary input (rather than written directly in an AST-construction call) could hit this.

Compatibility

  • Subject targets (Coder core project): net10.0;net9.0
  • ktsu.CaseConverter targets: multi-targets down to netstandard2.0 (via ktsu.Sdk), so it covers Coder's frameworks
  • Dependency direction: ktsu.CaseConverter has zero ktsu.* package references (confirmed via its Directory.Packages.props), so no cycle with ktsu.Coder

Sketch

- internal static string Spell(string words, NamingStyle style)
- {
-     string[] parts = [.. words.Split(' ').Where(part => part.Length > 0)];
-     if (parts.Length == 0) return string.Empty;
-     return style switch
-     {
-         NamingStyle.Snake => string.Join("_", parts.Select(part => part.ToLowerInvariant())),
-         NamingStyle.Camel => Lower(parts[0]) + string.Concat(parts.Skip(1).Select(Upper)),
-         _ => string.Concat(parts.Select(Upper)),
-     };
- }
+ internal static string Spell(string words, NamingStyle style) => style switch
+ {
+     NamingStyle.Snake => words.ToSnakeCase(),
+     NamingStyle.Camel => words.ToCamelCase(),
+     _ => words.ToPascalCase(),
+ };

Caveats

  • Adds a package reference (ktsu.CaseConverter) for a currently-internal, single-call-site helper — purely an implementation swap, no public API change.
  • CaseConverter's splitter treats any non-alphanumeric character as a word boundary (not just spaces), so it's slightly more permissive than Spell's literal ' ' split. Not observable at the current call site, since both inputs are always plain space-joined words, but worth noting if Spell ever gets a caller that passes punctuation.
  • NamingStyle has no Kebab/Macro members, so only three of CaseConverter's six conversions would be used.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions