VSDiagnostics has an analyzer that verifies whether or not a given identifier is following naming conventions. The following conventions are supported:
- UpperCamelCase
- lowerCamelCase
- _lowerCamelCase
- ICamelCase
On top of simply verifying this convention is followed it also suggests a new name that does follow conventions. The way I have implemented the verification is simple: simply generate a new identifier that follows conventions and check whether it's the same as the existing one.
Now, the interesting work is done inside the generating of this new identifier of course.
The approach I have thought out is the following:
Any identifier is basically a group of 0, 1 or more "sections". A section is denoted by special characters (anything that isn't a letter or a number) and capital letters that are preceded by a lower-case letter. For example in
My_var
,MyVar
andMY_VAR
all identifiers have two sections:My/var
,My/Var
andMY/VAR
. Once everything is grouped in sections we have to apply the proper naming convention. This is now made easy because everything ends with '-CamelCase' meaning we only have to apply the naming convention to the first section and all the remaining ones can have the 'Upper' convention.
Written in code that gives you the following implementation: Github
/// <summary>
/// Removes all non-digit, non-alphabetic characters. The naming convention of the first section can be specified, all
/// others use <see cref="NamingConvention.UpperCamelCase" />.
/// For example:
/// input = "_allo_ello"; first section = "allo"
/// input = "IBufferMyBuffer"; first section = "IBuffer"
/// input = "MY_SNAKE_CASE"; first section = "MY"
/// This allows us to remove things like underscores and have the individual sections they denoted in a proper
/// convention as well
/// </summary>
/// <param name="input"></param>
/// <param name="getFirstEntryConventioned"></param>
/// <returns></returns>
internal static string GetNormalizedString(string input, Func<string, string> getFirstEntryConventioned)
{
var sections = new List<string>();
var tempBuffer = new StringBuilder();
Action addSection = () =>
{
if (tempBuffer.Length != 0)
{
sections.Add(tempBuffer.ToString());
}
tempBuffer.Clear();
};
var previousCharWasUpper = false;
for (var i = 0; i < input.Length; i++)
{
var currChar = input[i];
if (char.IsLetter(currChar) || char.IsNumber(currChar))
{
var isCurrentCharUpper = char.IsUpper(currChar);
if (isCurrentCharUpper && !previousCharWasUpper)
{
addSection();
}
previousCharWasUpper = isCurrentCharUpper;
tempBuffer.Append(currChar);
}
else
{
// We already have data to add
// Existing data gets added but the current character is being ignored
addSection();
}
}
// If there is stuff remaining in the buffer, flush it as the last section
addSection();
// Identifiers that consist solely of underscores, e.g. _____
if (sections.Count == 0)
{
return input;
}
var sb = new StringBuilder(getFirstEntryConventioned(sections[0]));
for (var i = 1; i < sections.Count; i++)
{
sb.Append(Upper(sections[i]));
}
return sb.ToString();
}
internal static string Upper(string input)
{
if (input.Length == 0)
{
return string.Empty;
}
return char.ToUpper(input[0]) + input.Substring(1).ToLowerInvariant();
}
internal static string Lower(string input)
{
if (input.Length == 0)
{
return string.Empty;
}
return input.ToLowerInvariant();
}
internal static string IUpper(string input)
{
if (input.Length == 0)
{
return string.Empty;
}
if (input.Length == 1)
{
if (string.Equals("I", input, StringComparison.OrdinalIgnoreCase))
{
return "I";
}
return "I" + input.ToUpperInvariant();
}
if (input.StartsWith("I", StringComparison.OrdinalIgnoreCase))
{
return "I" + char.ToUpper(input[1]) + input.Substring(2).ToLowerInvariant();
}
return "I" + char.ToUpper(input[0]) + input.Substring(1).ToLowerInvariant();
}
internal static string UnderscoreLower(string input)
{
if (input.Length == 0)
{
return string.Empty;
}
return "_" + Lower(input);
}
This has been tested here and here
Apparently I have already asked about it. This is a rewrite of that implementation.
1 Answer 1
At first I didn't really like the implementation with the local
Action
but upon trying to re-implement it I realized it's difficult to restructure the code without making it actually more confusing - so fair enough.GetNormalizedString
can't deal with any of it's parameters beingnull
. Forinput
you could check forstring.IsNullOrWhiteSpace
and return or throw anArgumentNullException
.getFirstEntryConventioned
should probably have anArgumentNullExeption
thrown.Same goes for the naming convention functions: You only check for
input.Length == 0
. This means you're never expectingnull
arguments in which case you should use either use contracts or throwArgumentNullException
s or return empty strings.However given that this is a plugin meant to be used in Visual Studio as you edit you probably don't want exceptions bubbling up, so dealing with
null
inputs might be desirable from a user perspective.I think
IUpper
can be simplified a little bit:internal static string IUpper(string input) { if (input.Length == 0) { return string.Empty; } if (input.StartsWith("I", StringComparison.OrdinalIgnoreCase)) { input = input.Substring(1); } return "I" + Upper(input); }
Essentially, remove a leading
I
if there is one and then reduce it to theUpper
case.
-
1\$\begingroup\$ Regarding
IUpper
, if one has an interface calledInstaller
it should IMO returnIInstaller
. So removing the startingI
wouldn't be that good. \$\endgroup\$Heslacher– Heslacher2016年02月15日 08:33:25 +00:00Commented Feb 15, 2016 at 8:33