// 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.Collections; 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()); } |