YouCantSpell / src / YouCantSpell.ReSharper.Shared / JavaScript / JavaScriptSpellingSuggestionFormatter.cs

Full commit
using System;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.Naming;
using JetBrains.ReSharper.Psi.Naming.Impl;
using JetBrains.ReSharper.Psi.Naming.Interfaces;
using JetBrains.ReSharper.Psi.Naming.Settings;
using JetBrains.ReSharper.Psi.Tree;
using YouCantSpell.Utility;

namespace YouCantSpell.ReSharper.JavaScript
	[Obsolete("Merge with CSharpSpellingSuggestionFormatter or maybe it should be the C-Style naming formatter")]
	public class JavaScriptSpellingSuggestionFormatter

		/// <summary>
		/// Force the casing of the first character in a string to the desired case.
		/// </summary>
		/// <param name="text">The text to adjust.</param>
		/// <param name="textCase">The case to adjust the first letter to.</param>
		/// <returns>The adjusted string.</returns>
		private static string ForceFirstCharCase(string text, TextCaseClassification textCase) {
				return text;

			char firstLetter;
			switch(textCase) {
			case TextCaseClassification.Upper: { firstLetter = Char.ToUpper(text[0]); break; }
			case TextCaseClassification.Lower: { firstLetter = Char.ToLower(text[0]); break; }
			default: return text;

			if(firstLetter == text[0])
				return text; // it was already correct, so just return it

			return String.Concat(firstLetter, text.Length > 1 ? text.Substring(1) : String.Empty);


		/// <summary>
		/// Determines the case of the first letter of the second word within an identifier of the given name style.
		/// </summary>
		/// <param name="kind">The name style.</param>
		/// <returns>The case of the first letter of the second word within an identifier.</returns>
		private static TextCaseClassification SecondWordFirstLetterClassification(NamingStyleKinds kind) {
			switch(kind) {
			case NamingStyleKinds.AaBb:
			case NamingStyleKinds.AA_BB:
			case NamingStyleKinds.aaBb:
				return TextCaseClassification.Upper;
			case NamingStyleKinds.Aa_bb:
			case NamingStyleKinds.aa_bb:
				return TextCaseClassification.Lower;
				return TextCaseClassification.Unknown;

		private readonly NamingRule _namingRule;
		private readonly NameParser _nameParser;
		private readonly PsiLanguageType _psiLanguageType;
		private readonly INamingPolicyProvider _namingPolicyProvider;
		private readonly int _wordCount;
		private readonly TextCaseClassification _textClassification;

		public JavaScriptSpellingSuggestionFormatter(
			string identifierName,
			NamingRule namingRule,
			NameParser nameParser,
			PsiLanguageType psiLanguageType,
			INamingPolicyProvider namingPolicyProvider,
			int wordCount
		) {
				throw new ArgumentNullException("identifierName");
			if(null == namingRule)
				throw new ArgumentNullException("namingRule");
			if(null == nameParser)
				throw new ArgumentNullException("nameParser");

			_namingRule = namingRule;
			_nameParser = nameParser;
			_psiLanguageType = psiLanguageType;
			_namingPolicyProvider = namingPolicyProvider;
			_wordCount = wordCount;
			_textClassification = StringUtil.ClassifyCharCase(identifierName);

		/// <summary>
		/// Formats a suggestion to match identifier naming conventions.
		/// </summary>
		/// <param name="suggestion">The text suggestion.</param>
		/// <param name="position">The zero based word index within the identifier that the suggestion is for.</param>
		/// <returns>The suggestion formatted in a way so as to respect the configured naming conventions.</returns>
		public string FormatSuggestion(string suggestion, int position) {
			// Get the word parts of the suggestion.
			var matches = StringUtil.LetterParserRegex.Matches(suggestion).Cast<Match>().Select(x => x.Value).ToArray();
			if(matches.Length == 0)
				return suggestion;

			// Use resharper to reformat the word parts
			var newSuggestion = _nameParser.RenderNameSafe(
				NameRoot.FromWords(Emphasis.Unknown, false, matches),

			var literalIsOk = _wordCount == 1; // literals (@object) are only OK if there is one word in the identifier

			// after rendering the suggestion as an identifier name we need to parse it again to remove some stuff
			// just in case anything funny like a prefix or @ was added we need to get rid of that (unless @ is OK)
			var reparsedNamed = _nameParser.Parse(newSuggestion, NamingRule.Default, _namingPolicyProvider);
			if(reparsedNamed.NamePrefix.Text.Length > 0 && (!literalIsOk || reparsedNamed.NamePrefix.Text != "@")) {
				// we need to pull the prefix off except for one special situation, with literals, if that is a prefix
				newSuggestion = newSuggestion.Substring(reparsedNamed.NamePrefix.Text.Length);
			if(!literalIsOk && newSuggestion.Length > 1 && newSuggestion.StartsWith("@"))
				newSuggestion = newSuggestion.Substring(1); // if the @ prefix is not OK we need to get rid of that

			// Preserve the ugly all upper-case variables by forcing the new identifier part to uppercase if the identifier was uppercase to begin with
			if(_textClassification == TextCaseClassification.Upper && _namingRule.NamingStyleKind != NamingStyleKinds.AA_BB)
				return newSuggestion.ToUpper();

			// Don't know what to do if the suggestion was made to be blank
				return newSuggestion;

			// the first word within an identifier often has special casing that differs from the other words: firstSecondThird
			if (0 != position) {
				// Here we may need to correct the first letter if this is not the first word as the naming provider assumes the first word.
				// The R# methods assume the words I am giving it are the entire identifier name so the first letter may not be correct for
				// a suggestion derived from the 2nd word part within an identifier, we will need to adjust that.
				newSuggestion = ForceFirstCharCase(

			return newSuggestion;