File: NameFixProvider.cs
Web Access
Project: src\tools-local\Microsoft.ML.InternalCodeAnalyzer\Microsoft.ML.InternalCodeAnalyzer.csproj (Microsoft.ML.InternalCodeAnalyzer)
// 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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Rename;
 
namespace Microsoft.ML.InternalCodeAnalyzer
{
    // This is somewhat difficult. The trouble is, if a name is in a bad state, it is
    // actually rather difficult to come up with a general procedure to "fix" it. We
    // instead focus on the common case where a name is right according to *some* sort
    // of regular scheme, and focus on that.
 
    using Debug = System.Diagnostics.Debug;
 
    [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(NameFixProvider)), Shared]
    public sealed class NameFixProvider : CodeFixProvider
    {
        private const string PrivateTitle = "Fix name";
 
        private static ImmutableArray<string> _fixable = ImmutableArray.Create(
            NameAnalyzer.PrivateFieldName.Id, NameAnalyzer.GeneralName.Id,
            ParameterVariableNameAnalyzer.Id, TypeParamNameAnalyzer.Id);
 
        private static readonly Regex _sections = new Regex(
            @"(?:\p{Nd}\p{Ll}*)|" + // Numbers we consider a separate token.
            @"(?:\p{Lu}+(?!\p{Ll}))|" + // Completely upper case sections.
            @"(?:\p{Lu}\p{Ll}+)|" + // Title cased word.
            @"(?:\p{Ll}+)"); // Lower case word.
 
        public override ImmutableArray<string> FixableDiagnosticIds => _fixable;
 
        public override FixAllProvider GetFixAllProvider()
            => WellKnownFixAllProviders.BatchFixer;
 
        public override async Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var diagnostic = context.Diagnostics[0];
 
            string originalName = diagnostic.Properties[NameAnalyzer.NameProperty];
            string desiredNameStr = diagnostic.Properties[NameAnalyzer.DesiredNameProperty];
            NameType desiredName;
            if (!Enum.TryParse(desiredNameStr, out desiredName))
                return;
 
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
            var token = root.FindToken(diagnostic.Location.SourceSpan.Start);
            if (token.Text != originalName)
                return;
 
            string docName = context.Document.Name;
            if (docName.Length - 3 == originalName.Length && docName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)
                && context.Document.Name.StartsWith(originalName, StringComparison.OrdinalIgnoreCase))
            {
                // So this is an entity like "FooBarBiz" in a file named "FooBarBiz.cs".
                // We can continue to warn on these, but registering a *fix* for them would
                // be inappropriate, since while the Roslyn API allows us to rename items like,
                // these, we cannot change the file name.
                return;
            }
 
            Func<string, string> renamer = null;
            switch (desiredName)
            {
                case NameType.UnderScoreCamelCased:
                    renamer = RenameUnderscore;
                    break;
                case NameType.CamelCased:
                    renamer = RenameCamelCase;
                    break;
                case NameType.PascalCased:
                    renamer = RenamePascal;
                    break;
                case NameType.IPascalCased:
                    renamer = RenameInterface;
                    break;
                case NameType.TPascalCased:
                    renamer = RenameTypeParam;
                    break;
                default:
                    Debug.Assert(!Enum.IsDefined(typeof(NameType), desiredName));
                    break;
            }
 
            context.RegisterCodeFix(CodeAction.Create(PrivateTitle,
                c => RenameAsync(context.Document, token.Parent, originalName, renamer, c), diagnostic.Id), diagnostic);
        }
 
        private async Task<Solution> RenameAsync(Document document,
            SyntaxNode identifier, string name, Func<string, string> renamer, CancellationToken cancellationToken)
        {
            // Get the symbol representing the type to be renamed.
            var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
            ISymbol typeSymbol = semanticModel.GetDeclaredSymbol(identifier, cancellationToken);
 
            string newName = renamer(name);
 
            // Produce a new solution that has all references to that type renamed, including the declaration.
            var originalSolution = document.Project.Solution;
            var optionSet = originalSolution.Workspace.Options;
            var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, newName, optionSet, cancellationToken).ConfigureAwait(false);
 
            // Return the new solution with the now-uppercase type name.
            return newSolution;
        }
 
        private IEnumerable<string> ExtractSections(string name)
        {
            foreach (Match match in _sections.Matches(name))
                yield return match.Value;
        }
 
        private string RenameUnderscore(string name) => RenameCamelCore(name, "_");
        private string RenameCamelCase(string name) => RenameCamelCore(name, "");
        private string RenameTypeParam(string name) => RenamePascalPrefixCore(name, "T");
        private string RenameInterface(string name) => RenamePascalPrefixCore(name, "I");
        private string RenamePascal(string name) => RenamePascalPrefixCore(name, "");
 
        private string RenameCamelCore(string name, string prefix)
        {
            if (string.IsNullOrEmpty(name))
                return prefix;
            StringBuilder sb = new StringBuilder(prefix);
            foreach (var section in ExtractSections(name))
            {
                if (sb.Length == prefix.Length)
                    sb.Append(section.ToLowerInvariant());
                else
                    AppendTitleCase(sb, section);
            }
            return sb.ToString();
        }
 
        private string RenamePascalPrefixCore(string name, string prefix)
        {
            if (string.IsNullOrEmpty(name))
                return prefix;
            StringBuilder sb = new StringBuilder(prefix);
            bool first = true;
            foreach (var section in ExtractSections(name))
            {
                if (first)
                {
                    first = false;
                    if (prefix == section)
                        continue;
                }
                AppendTitleCase(sb, section);
            }
            return sb.ToString();
        }
 
        private void AppendTitleCase(StringBuilder builder, string token)
        {
            if (string.IsNullOrEmpty(token))
                return;
            if (token.Length == 2 && char.IsUpper(token[0]) && char.IsUpper(token[1]))
            {
                builder.Append(token);
                return;
            }
            // Further special casing for things like: IO, UI?
            builder.Append(char.ToUpperInvariant(token[0]));
            builder.Append(token.Substring(1).ToLowerInvariant());
        }
    }
}