You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Roslyn Renamer未重命名接口上的方法调用问题

问题描述

我开发了一款处理WCF连接服务生成代码的CLI工具,WSDL中的所有操作和消息均采用驼峰(camelCase)命名,因此需要将其修改为帕斯卡(PascalCase)命名。功能整体运行正常但速度较慢,目前存在一个核心问题:使用Roslyn的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);
    }
}

必要依赖包:

  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.CSharp.Workspaces

运行后会发现,结果代码中的base.Channel.getDataAsync(request)未被重命名,且可确认base.Channel的类型为Test.web。


问题原因

  1. 注解关联失效:预先添加的FullyQualifiedName注解无法同步到Renamer生成的新语法树节点上,导致后续无法正确定位需要重命名的符号。
  2. 重命名顺序错误:先重命名类型再处理成员,类型名称变更后,成员的旧全限定名无法匹配新语法树中的节点,导致接口方法声明未被正确重命名,对应的调用自然不会更新。
  3. 重复语义分析:循环中频繁重新获取语义模型,大幅拖慢了处理速度。

解决方案

修复后的核心代码

调整重命名逻辑,直接通过语义模型追踪符号,批量处理重命名以提升效率:

public static async Task<string> RenameIdentifiersToPascalCaseAsync(string sourceCode)
{
    var workspace = new AdhocWorkspace();
    var project = workspace.CurrentSolution.AddProject("TempProject", "TempAssembly", LanguageNames.CSharp);
    var document = project.AddDocument("TempDocument", sourceCode);
    var solution = document.Project.Solution;

    var semanticModel = await document.GetSemanticModelAsync();
    var root = await semanticModel.SyntaxTree.GetRootAsync();

    var options = new SymbolRenameOptions();
    var symbolsToRename = new List<ISymbol>();

    // 先收集所有需要重命名的成员(方法、属性)
    foreach (var memberNode in root.DescendantNodes().OfType<MemberDeclarationSyntax>()
        .Where(n => n is MethodDeclarationSyntax or PropertyDeclarationSyntax))
    {
        var symbol = semanticModel.GetDeclaredSymbol(memberNode);
        if (symbol != null && symbol.Name != symbol.Name.ToPascalCase())
        {
            symbolsToRename.Add(symbol);
        }
    }

    // 再收集所有需要重命名的类型(类、接口)
    foreach (var typeNode in root.DescendantNodes().OfType<BaseTypeDeclarationSyntax>())
    {
        var symbol = semanticModel.GetDeclaredSymbol(typeNode);
        if (symbol != null && symbol.Name != symbol.Name.ToPascalCase())
        {
            symbolsToRename.Add(symbol);
        }
    }

    // 批量执行重命名
    foreach (var symbol in symbolsToRename)
    {
        var newName = symbol.Name.ToPascalCase();
        solution = await Renamer.RenameSymbolAsync(solution, symbol, options, newName);
    }

    // 获取最终代码
    var newDocument = solution.Projects.Single().Documents.Single();
    var newRoot = await newDocument.GetSyntaxRootAsync();
    return newRoot.ToFullString();
}

优化说明

  1. 移除注解逻辑:直接通过语义模型获取符号,避免注解失效问题,简化代码。
  2. 调整重命名顺序:先处理成员再处理类型,确保类型名称变更前,成员符号已被正确追踪和重命名。
  3. 批量处理:一次性收集所有需要重命名的符号,减少重复的语义分析操作,提升处理速度。
  4. 添加重命名判断:仅当名称需要变更时才执行重命名,避免无意义操作。

修改后运行代码,base.Channel.getDataAsync(request)会被正确重命名为base.Channel.GetDataAsync(request),所有驼峰命名的类型和成员都会转换为帕斯卡格式。

内容的提问来源于stack exchange,提问作者Marcel Lorenz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 21:25:14