File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\NamingStyles\NamingStyle.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.NamingStyles;
 
[DataContract]
internal readonly partial record struct NamingStyle
{
    [DataMember(Order = 0)]
    public Guid ID { get; init; }
 
    [DataMember(Order = 1)]
    public string Name { get; init; }
 
    [DataMember(Order = 2)]
    public string Prefix { get; init; }
 
    [DataMember(Order = 3)]
    public string Suffix { get; init; }
 
    [DataMember(Order = 4)]
    public string WordSeparator { get; init; }
 
    [DataMember(Order = 5)]
    public Capitalization CapitalizationScheme { get; init; }
 
    public NamingStyle(
        Guid id,
        string name = null,
        string prefix = null,
        string suffix = null,
        string wordSeparator = null,
        Capitalization capitalizationScheme = Capitalization.PascalCase)
    {
        ID = id;
        Name = name;
        Prefix = prefix ?? "";
        Suffix = suffix ?? "";
        WordSeparator = wordSeparator ?? "";
        CapitalizationScheme = capitalizationScheme;
    }
 
    public string CreateName(ImmutableArray<string> words)
    {
        var wordsWithCasing = ApplyCapitalization(words);
        var combinedWordsWithCasing = string.Join(WordSeparator, wordsWithCasing);
        return Prefix + combinedWordsWithCasing + Suffix;
    }
 
    private IEnumerable<string> ApplyCapitalization(IEnumerable<string> words)
    {
        switch (CapitalizationScheme)
        {
            case Capitalization.PascalCase:
                return words.Select(CapitalizeFirstLetter);
            case Capitalization.CamelCase:
                return words.Take(1).Select(DecapitalizeFirstLetter).Concat(words.Skip(1).Select(CapitalizeFirstLetter));
            case Capitalization.FirstUpper:
                return words.Take(1).Select(CapitalizeFirstLetter).Concat(words.Skip(1).Select(DecapitalizeFirstLetter));
            case Capitalization.AllUpper:
                return words.Select(w => w.ToUpper());
            case Capitalization.AllLower:
                return words.Select(w => w.ToLower());
            default:
                throw new InvalidOperationException();
        }
    }
 
    private string CapitalizeFirstLetter(string word)
    {
        if (word.Length == 0)
        {
            return word;
        }
 
        if (char.IsUpper(word[0]))
        {
            return word;
        }
 
        var chars = word.ToCharArray();
        chars[0] = char.ToUpper(chars[0]);
 
        return new string(chars);
    }
 
    private string DecapitalizeFirstLetter(string word)
    {
        if (word.Length == 0)
        {
            return word;
        }
 
        if (char.IsLower(word[0]))
        {
            return word;
        }
 
        var chars = word.ToCharArray();
        chars[0] = char.ToLower(chars[0]);
 
        return new string(chars);
    }
 
    public bool IsNameCompliant(string name, out string failureReason)
    {
        if (!name.StartsWith(Prefix))
        {
            failureReason = string.Format(CompilerExtensionsResources.Missing_prefix_colon_0, Prefix);
            return false;
        }
 
        if (!name.EndsWith(Suffix))
        {
            failureReason = string.Format(CompilerExtensionsResources.Missing_suffix_colon_0, Suffix);
            return false;
        }
 
        if (name.Length <= Prefix.Length + Suffix.Length)
        {
            // name consists of Prefix and Suffix and no base name
            // Prefix and Suffix can overlap
            // Example: Prefix = "s_", Suffix = "_t", name "s_t"
            failureReason = null;
            return true;
        }
 
        // remove specified Prefix, then look for any other common prefixes
        name = StripCommonPrefixes(name[Prefix.Length..], out var prefix);
 
        if (prefix != string.Empty)
        {
            // name started with specified prefix, but has at least one additional common prefix 
            // Example: specified prefix "test_", actual prefix "test_m_"
            failureReason = Prefix == string.Empty
                ? string.Format(CompilerExtensionsResources.Prefix_0_is_not_expected, prefix)
                : string.Format(CompilerExtensionsResources.Prefix_0_does_not_match_expected_prefix_1, prefix, Prefix);
            return false;
        }
 
        // specified and common prefixes have been removed. Now see that the base name has correct capitalization
        var spanToCheck = TextSpan.FromBounds(0, name.Length - Suffix.Length);
        Debug.Assert(spanToCheck.Length > 0);
 
        switch (CapitalizationScheme)
        {
            case Capitalization.PascalCase: return CheckPascalCase(name, spanToCheck, out failureReason);
            case Capitalization.CamelCase: return CheckCamelCase(name, spanToCheck, out failureReason);
            case Capitalization.FirstUpper: return CheckFirstUpper(name, spanToCheck, out failureReason);
            case Capitalization.AllUpper: return CheckAllUpper(name, spanToCheck, out failureReason);
            case Capitalization.AllLower: return CheckAllLower(name, spanToCheck, out failureReason);
            default: throw new InvalidOperationException();
        }
    }
 
    private WordSpanEnumerable GetWordSpans(string name, TextSpan nameSpan)
        => new(name, nameSpan, WordSeparator);
 
    private static string Substring(string name, TextSpan wordSpan)
        => name.Substring(wordSpan.Start, wordSpan.Length);
 
    private static readonly Func<string, TextSpan, bool> s_firstCharIsLowerCase = (val, span) => !DoesCharacterHaveCasing(val[span.Start]) || char.IsLower(val[span.Start]);
    private static readonly Func<string, TextSpan, bool> s_firstCharIsUpperCase = (val, span) => !DoesCharacterHaveCasing(val[span.Start]) || char.IsUpper(val[span.Start]);
 
    private static readonly Func<string, TextSpan, bool> s_wordIsAllUpperCase = (val, span) =>
    {
        for (int i = span.Start, n = span.End; i < n; i++)
        {
            if (DoesCharacterHaveCasing(val[i]) && !char.IsUpper(val[i]))
            {
                return false;
            }
        }
 
        return true;
    };
 
    private static readonly Func<string, TextSpan, bool> s_wordIsAllLowerCase = (val, span) =>
    {
        for (int i = span.Start, n = span.End; i < n; i++)
        {
            if (DoesCharacterHaveCasing(val[i]) && !char.IsLower(val[i]))
            {
                return false;
            }
        }
 
        return true;
    };
 
    private bool CheckAllWords(
        string name, TextSpan nameSpan, Func<string, TextSpan, bool> wordCheck,
        string resourceId, out string reason)
    {
        reason = null;
        using var _ = ArrayBuilder<string>.GetInstance(out var violations);
 
        foreach (var wordSpan in GetWordSpans(name, nameSpan))
        {
            if (!wordCheck(name, wordSpan))
            {
                violations.Add(Substring(name, wordSpan));
            }
        }
 
        if (violations.Count > 0)
        {
            reason = string.Format(resourceId, string.Join(", ", violations));
        }
 
        return reason == null;
    }
 
    private bool CheckPascalCase(string name, TextSpan nameSpan, out string reason)
        => CheckAllWords(
            name, nameSpan, s_firstCharIsUpperCase,
            CompilerExtensionsResources.These_words_must_begin_with_upper_case_characters_colon_0, out reason);
 
    private bool CheckAllUpper(string name, TextSpan nameSpan, out string reason)
        => CheckAllWords(
            name, nameSpan, s_wordIsAllUpperCase,
            CompilerExtensionsResources.These_words_cannot_contain_lower_case_characters_colon_0, out reason);
 
    private bool CheckAllLower(string name, TextSpan nameSpan, out string reason)
        => CheckAllWords(
            name, nameSpan, s_wordIsAllLowerCase,
            CompilerExtensionsResources.These_words_cannot_contain_upper_case_characters_colon_0, out reason);
 
    private bool CheckFirstAndRestWords(
        string name, TextSpan nameSpan,
        Func<string, TextSpan, bool> firstWordCheck,
        Func<string, TextSpan, bool> restWordCheck,
        string firstResourceId,
        string restResourceId,
        out string reason)
    {
        reason = null;
        using var _ = ArrayBuilder<string>.GetInstance(out var violations);
 
        var first = true;
 
        foreach (var wordSpan in GetWordSpans(name, nameSpan))
        {
            if (first)
            {
                if (!firstWordCheck(name, wordSpan))
                {
                    reason = string.Format(firstResourceId, Substring(name, wordSpan));
                }
            }
            else
            {
                if (!restWordCheck(name, wordSpan))
                {
                    violations.Add(Substring(name, wordSpan));
                }
            }
 
            first = false;
        }
 
        if (violations.Count > 0)
        {
            var restString = string.Format(restResourceId, string.Join(", ", violations));
            reason = reason == null
                ? restString
                : reason + Environment.NewLine + restString;
        }
 
        return reason == null;
    }
 
    private bool CheckCamelCase(string name, TextSpan nameSpan, out string reason)
        => CheckFirstAndRestWords(
            name, nameSpan, s_firstCharIsLowerCase, s_firstCharIsUpperCase,
            CompilerExtensionsResources.The_first_word_0_must_begin_with_a_lower_case_character,
            CompilerExtensionsResources.These_non_leading_words_must_begin_with_an_upper_case_letter_colon_0,
            out reason);
 
    private bool CheckFirstUpper(string name, TextSpan nameSpan, out string reason)
        => CheckFirstAndRestWords(
            name, nameSpan, s_firstCharIsUpperCase, s_firstCharIsLowerCase,
            CompilerExtensionsResources.The_first_word_0_must_begin_with_an_upper_case_character,
            CompilerExtensionsResources.These_non_leading_words_must_begin_with_a_lowercase_letter_colon_0,
            out reason);
 
    private static bool DoesCharacterHaveCasing(char c) => char.ToLower(c) != char.ToUpper(c);
 
    private string CreateCompliantNameDirectly(string name)
    {
        // Example: for specified prefix = "Test_" and name = "Test_m_BaseName", we remove "Test_m_"
        // "Test_" will be added back later in this method
        name = StripCommonPrefixes(name.StartsWith(Prefix) ? name[Prefix.Length..] : name, out _);
 
        var addPrefix = !name.StartsWith(Prefix);
        var addSuffix = !name.EndsWith(Suffix);
 
        name = addPrefix ? (Prefix + name) : name;
        name = addSuffix ? (name + Suffix) : name;
 
        return FinishFixingName(name);
    }
 
    public IEnumerable<string> MakeCompliant(string name)
    {
        var name1 = CreateCompliantNameReusingPartialPrefixesAndSuffixes(name);
        yield return name1;
 
        var name2 = CreateCompliantNameDirectly(name);
        if (name2 != name1)
        {
            yield return name2;
        }
    }
 
    private string CreateCompliantNameReusingPartialPrefixesAndSuffixes(string name)
    {
        name = StripCommonPrefixes(name, out _);
        name = EnsurePrefix(name);
        name = EnsureSuffix(name);
 
        return FinishFixingName(name);
    }
 
    public static string StripCommonPrefixes(string name, out string prefix)
    {
        var index = 0;
        while (index + 1 < name.Length)
        {
            switch (char.ToLowerInvariant(name[index]))
            {
                case 'm':
                case 's':
                case 't':
                    if (index + 2 < name.Length && name[index + 1] == '_')
                    {
                        index++;
                        continue;
                    }
 
                    break;
 
                case '_':
                    if (index + 1 < name.Length && !char.IsDigit(name[index + 1]))
                    {
                        index++;
                        continue;
                    }
 
                    break;
 
                default:
                    break;
            }
 
            // If we reach this point, the current iteration did not strip any additional characters
            break;
        }
 
        prefix = name[..index];
        return name[index..];
    }
 
    private string FinishFixingName(string name)
    {
        // Edge case: prefix "as", suffix "sa", name "asa"
        if (Suffix.Length + Prefix.Length >= name.Length)
        {
            return name;
        }
 
        name = name[Prefix.Length..^Suffix.Length];
        IEnumerable<string> words = [name];
 
        if (!string.IsNullOrEmpty(WordSeparator))
        {
            words = name.Split([WordSeparator], StringSplitOptions.RemoveEmptyEntries);
 
            // Edge case: the only character(s) in the name is(are) the WordSeparator
            if (!words.Any())
            {
                return name;
            }
 
            if (words.Count() == 1) // Only Split if words have not been split before 
            {
                var isWord = true;
                using var parts = TemporaryArray<TextSpan>.Empty;
                StringBreaker.AddParts(name, isWord, ref parts.AsRef());
                var newWords = new string[parts.Count];
                for (var i = 0; i < parts.Count; i++)
                {
                    newWords[i] = name[parts[i].Start..parts[i].End];
                }
 
                words = newWords;
            }
        }
 
        words = ApplyCapitalization(words);
 
        return Prefix + string.Join(WordSeparator, words) + Suffix;
    }
 
    private string EnsureSuffix(string name)
    {
        // If the name already ends with any prefix of the Suffix, only append the suffix of
        // the Suffix not contained in the longest such Suffix prefix. For example, if the 
        // required suffix is "_catdog" and the name is "test_cat", then only append "dog".
        for (var i = Suffix.Length; i > 0; i--)
        {
            if (name.EndsWith(Suffix[..i]))
                return name + Suffix[i..];
        }
 
        return name + Suffix;
    }
 
    private string EnsurePrefix(string name)
    {
        // Exceptional cases.  If the name is some interface name (like `InputStream`) and the rule is to have a single
        // character prefix like "Add `I` for interfaces" don't consider the existing 'I' to be a match of the prefix.
 
        if (Prefix is [var prefixChar] &&
            char.IsUpper(prefixChar) &&
            name is [var nameChar1, var nameChar2, ..] &&
            prefixChar == nameChar1 &&
            char.IsLower(nameChar2))
        {
            // return IInputStream here, even though InputStream already starts with 'I'.
            return Prefix + name;
        }
 
        // If the name already starts with any suffix of the Prefix, only prepend the prefix of
        // the Prefix not contained in the longest such Prefix suffix. For example, if the 
        // required prefix is "catdog_" and the name is "dog_test", then only prepend "cat".
        for (var i = 0; i < Prefix.Length; i++)
        {
            if (name.StartsWith(Prefix[i..]))
                return Prefix[..i] + name;
        }
 
        return Prefix + name;
    }
 
    internal XElement CreateXElement()
        => new(nameof(NamingStyle),
            new XAttribute(nameof(ID), ID),
            new XAttribute(nameof(Name), Name),
            new XAttribute(nameof(Prefix), Prefix ?? string.Empty),
            new XAttribute(nameof(Suffix), Suffix ?? string.Empty),
            new XAttribute(nameof(WordSeparator), WordSeparator ?? string.Empty),
            new XAttribute(nameof(CapitalizationScheme), CapitalizationScheme));
 
    internal static NamingStyle FromXElement(XElement namingStyleElement)
        => new(
            id: Guid.Parse(namingStyleElement.Attribute(nameof(ID)).Value),
            name: namingStyleElement.Attribute(nameof(Name)).Value,
            prefix: namingStyleElement.Attribute(nameof(Prefix)).Value,
            suffix: namingStyleElement.Attribute(nameof(Suffix)).Value,
            wordSeparator: namingStyleElement.Attribute(nameof(WordSeparator)).Value,
            capitalizationScheme: (Capitalization)Enum.Parse(typeof(Capitalization), namingStyleElement.Attribute(nameof(CapitalizationScheme)).Value));
 
    public void WriteTo(ObjectWriter writer)
    {
        writer.WriteGuid(ID);
        writer.WriteString(Name);
        writer.WriteString(Prefix ?? string.Empty);
        writer.WriteString(Suffix ?? string.Empty);
        writer.WriteString(WordSeparator ?? string.Empty);
        writer.WriteInt32((int)CapitalizationScheme);
    }
 
    public static NamingStyle ReadFrom(ObjectReader reader)
        => new(
            reader.ReadGuid(),
            reader.ReadString(),
            reader.ReadString(),
            reader.ReadString(),
            reader.ReadString(),
            (Capitalization)reader.ReadInt32());
}