Roslyn Renamer 不会重命名接口上的方法调用

问题描述 投票:0回答:1

我有一个小型 CLI 工具来处理 WCF 连接服务生成的代码。问题是 WSDL 中的所有操作和消息都以驼峰命名法命名。所以我想将它们重命名为 PascalCase。尽管速度相当慢,但效果很好。只有一个问题。我使用罗斯林

Microsoft.CodeAnalysis.Rename.Renamer
。当我重命名方法声明的符号时(在本例中是在接口中),该方法的调用不会被重命名。他们留在驼峰式大小写中。

最小可重复示例:

using System.Text.RegularExpressions;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Rename;

public static class Program {
    public static async Task Main(string[] args) {
        var code = """
            namespace Test {
                public class input {
                    public object data { get; set; }
                }

                public class serviceResponse {
                    public object data { get; set; }
                }

                public interface web {
                    System.Threading.Tasks.Task<Test.serviceResponse> getDataAsync(Test.input request);
                }

                public interface webChannel : Test.web, System.ServiceModel.IClientChannel {

                }

                public partial class webClient : System.ServiceModel.ClientBase<Test.web>, Test.web {
                    public System.Threading.Tasks.Task<Test.serviceResponse> getDataAsync(Test.input request)
                    {
                        return base.Channel.getDataAsync(request);
                    }
                }
            }
            """;

        var renamedCode = await RenameIdentifiersToPascalCaseAsync(code);
        Console.WriteLine(renamedCode);
    }

    public static async Task<string> RenameIdentifiersToPascalCaseAsync(string sourceCode) {
        const string annotationKey = "FullyQualifiedName";
        var preparationSyntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
        var preparationSyntaxTreeRoot = await preparationSyntaxTree.GetRootAsync();
        var nodesToReplace = preparationSyntaxTreeRoot.DescendantNodes()
            .Where(node => node is BaseTypeDeclarationSyntax or MethodDeclarationSyntax or PropertyDeclarationSyntax)
            .ToList();

        var preparedRootNode = preparationSyntaxTreeRoot
            .ReplaceNodes(nodesToReplace, (oldNode, newNode) => newNode.WithAdditionalAnnotations(new SyntaxAnnotation(annotationKey, GetFullyQualifiedName(newNode))));

        var document = new AdhocWorkspace().CurrentSolution.AddProject("TempProject", "TempAssembly", LanguageNames.CSharp).AddDocument("TempDocument", preparedRootNode);
        var project = document.Project;
        var solution = project.Solution;

        var semanticModel = await document.GetSemanticModelAsync();
        var syntaxTree = semanticModel!.SyntaxTree;

        var options = new SymbolRenameOptions();

        var typesDeclarations = syntaxTree.GetRoot().DescendantNodes().OfType<BaseTypeDeclarationSyntax>().ToList();
        Console.WriteLine("Renaming Types, Properties and Methods to Pascal Case...");

        for (var i = 0; i < typesDeclarations.Count; i++)
        {
            var typeDeclaration = typesDeclarations[i];

            var membersToRename = new List<MemberDeclarationSyntax> { typeDeclaration };
            membersToRename.AddRange(typeDeclaration.DescendantNodes().OfType<PropertyDeclarationSyntax>().ToList());
            membersToRename.AddRange(typeDeclaration.DescendantNodes().OfType<MethodDeclarationSyntax>().ToList());

            Console.WriteLine($"{(i + 1).ToString("D" + typesDeclarations.Count.ToString().Length)}/{typesDeclarations.Count} - Renaming {typeDeclaration.Identifier.Text}");
            foreach (var member in membersToRename)
            {
                semanticModel = await solution.Projects.First().Documents.First().GetSemanticModelAsync();

                var memberFullyQualifiedName = member.GetAnnotations(annotationKey).First().Data;
                var newMemberInstance = (await semanticModel!.SyntaxTree.GetRootAsync())
                    .DescendantNodesAndSelf()
                    .First(node => node.GetAnnotations(annotationKey).Any(a => a.Data == memberFullyQualifiedName));

                var typeSymbol = semanticModel.GetDeclaredSymbol(newMemberInstance);

                if (typeSymbol != null)
                {
                    var newName = typeSymbol.Name.ToPascalCase();
                    solution = await Renamer.RenameSymbolAsync(solution, typeSymbol, options, newName);
                }
            }
        }

        var newDocument = solution.Projects.Single().Documents.Single();
        var newSourceCode = (await newDocument!.GetSyntaxRootAsync())!.ToFullString();
        return newSourceCode;
    }
    
    private static string GetFullyQualifiedName(SyntaxNode node)
    {
        var name = node switch
        {
            NamespaceDeclarationSyntax => (node as NamespaceDeclarationSyntax)!.Name.ToString(),
            BaseTypeDeclarationSyntax => (node as BaseTypeDeclarationSyntax)!.Identifier.ToString(),
            PropertyDeclarationSyntax => (node as PropertyDeclarationSyntax)!.Identifier.ToString(),
            MethodDeclarationSyntax => (node as MethodDeclarationSyntax)!.Identifier.ToString(),
            _ => throw new ArgumentException(nameof(node))
        };

        if (node.Parent is null || node is NamespaceDeclarationSyntax)
        {
            return name;
        }

        return $"{GetFullyQualifiedName(node.Parent)}.{name}";
    }
}

// code from: https://stackoverflow.com/a/46095771/8304361
public static class StringExtensions
{
    private static Regex _invalidCharsRgx = new Regex("[^_a-zA-Z0-9]", RegexOptions.Compiled);
    private static Regex _whiteSpace = new Regex(@"(?<=\s)", RegexOptions.Compiled);
    private static Regex _startsWithLowerCaseChar = new Regex("^[a-z]", RegexOptions.Compiled);
    private static Regex _firstCharFollowedByUpperCasesOnly = new Regex("(?<=[A-Z])[A-Z0-9]+$", RegexOptions.Compiled);
    private static Regex _lowerCaseNextToNumber = new Regex("(?<=[0-9])[a-z]", RegexOptions.Compiled);
    private static Regex _upperCaseInside = new Regex("(?<=[A-Z])[A-Z]+?((?=[A-Z][a-z])|(?=[0-9]))", RegexOptions.Compiled);

    public static string ToPascalCase(this string s)
    {

        // replace white spaces with undescore, then replace all invalid chars with empty string
        var pascalCase = _invalidCharsRgx.Replace(_whiteSpace.Replace(s, "_"), string.Empty)
            // split by underscores
            .Split(new char[] { '_' }, StringSplitOptions.RemoveEmptyEntries)
            // set first letter to uppercase
            .Select(w => _startsWithLowerCaseChar.Replace(w, m => m.Value.ToUpper()))
            // replace second and all following upper case letters to lower if there is no next lower (ABC -> Abc)
            .Select(w => _firstCharFollowedByUpperCasesOnly.Replace(w, m => m.Value.ToLower()))
            // set upper case the first lower case following a number (Ab9cd -> Ab9Cd)
            .Select(w => _lowerCaseNextToNumber.Replace(w, m => m.Value.ToUpper()))
            // lower second and next upper case letters except the last if it follows by any lower (ABcDEf -> AbcDef)
            .Select(w => _upperCaseInside.Replace(w, m => m.Value.ToLower()));

        return string.Concat(pascalCase);
    }
}

所需包裹:

  1. Microsoft.CodeAnalysis.CSharp
  2. Microsoft.CodeAnalysis.CSharp.Workspaces

您会看到,在结果代码中

base.Channel.getDataAsync(request)
没有被重命名。我可以保证
base.Channel
属于类型
Test.web

.net roslyn-code-analysis
1个回答
0
投票

我找不到关于 Renamer 的好的文档,但我想 Renamer 基本上只是重命名符号而不是重命名依赖的方法调用。

我设法手动更改方法声明标识符并修改正文。它可能会给您有关解决方法的见解。

        if (typeSymbol != null)
        {
            typeSymbol = semanticModel.GetDeclaredSymbol(newMemberInstance);
            var newName = typeSymbol.Name.ToPascalCase();
            solution = await Renamer.RenameSymbolAsync(solution, typeSymbol, options, newName);
            if (member is MethodDeclarationSyntax methodDeclarationSyntax)
            {

                if (!string.IsNullOrEmpty(methodDeclarationSyntax.Body?.ToFullString()))
                {

                    var oldIdentifier = methodDeclarationSyntax.Identifier.ToFullString().ToPascalCase();
                    var renamedIdentifierName = SyntaxFactory.Identifier(oldIdentifier);
                   
                    /*Create new Method From Existing One With Tracer*/
                    var traceText =
                        @"System.Diagnostics.Trace.WriteLine(""Tracing: '" + methodDeclarationSyntax.Ancestors().OfType<NamespaceDeclarationSyntax>().Single().Name + "." + methodDeclarationSyntax.Identifier.ValueText + "'\");";
                    var traceStatement = SyntaxFactory.ParseStatement(traceText);
                    
                    var blockSyntax = methodDeclarationSyntax.Body;
                    var bodyStatementsWithTrace = blockSyntax.Statements.Insert(0, traceStatement);
                    var newBody = blockSyntax.Update(blockSyntax.OpenBraceToken, bodyStatementsWithTrace,
                        blockSyntax.CloseBraceToken);
                    
                    
                    var newMethod = methodDeclarationSyntax.ReplaceNode(methodDeclarationSyntax.Body, newBody);
                    //Update Method Identifier
                    newMethod = newMethod.ReplaceToken(newMethod.Identifier, renamedIdentifierName);

                    
                    //Update Syntax Node,Document & Solution
                    var docRoot = await currentDocument.GetSyntaxRootAsync();
                    var newDocRoot = docRoot.ReplaceNode(newMemberInstance, newMethod);
                    var newDoc = currentDocument.WithSyntaxRoot(newDocRoot);
                    var newDocText = await newDoc.GetTextAsync();
                    solution = solution.WithDocumentText(newDoc.Id, newDocText);
                }

© www.soinside.com 2019 - 2024. All rights reserved.