7
\$\begingroup\$

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 and MY_VAR all identifiers have two sections: My/var, My/Var and MY/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.

asked Feb 10, 2016 at 21:35
\$\endgroup\$

1 Answer 1

3
+25
\$\begingroup\$
  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.

  2. GetNormalizedString can't deal with any of it's parameters being null. For input you could check for string.IsNullOrWhiteSpace and return or throw an ArgumentNullException. getFirstEntryConventioned should probably have an ArgumentNullExeption thrown.

    Same goes for the naming convention functions: You only check for input.Length == 0. This means you're never expecting null arguments in which case you should use either use contracts or throw ArgumentNullExceptions 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.

  3. 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 the Upper case.

answered Feb 15, 2016 at 8:14
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Regarding IUpper, if one has an interface called Installer it should IMO return IInstaller. So removing the starting I wouldn't be that good. \$\endgroup\$ Commented Feb 15, 2016 at 8:33

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.