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.CSharpMicrosoft.CodeAnalysis.CSharp.Workspaces
运行后会发现,结果代码中的base.Channel.getDataAsync(request)未被重命名,且可确认base.Channel的类型为Test.web。
问题原因
- 注解关联失效:预先添加的
FullyQualifiedName注解无法同步到Renamer生成的新语法树节点上,导致后续无法正确定位需要重命名的符号。 - 重命名顺序错误:先重命名类型再处理成员,类型名称变更后,成员的旧全限定名无法匹配新语法树中的节点,导致接口方法声明未被正确重命名,对应的调用自然不会更新。
- 重复语义分析:循环中频繁重新获取语义模型,大幅拖慢了处理速度。
解决方案
修复后的核心代码
调整重命名逻辑,直接通过语义模型追踪符号,批量处理重命名以提升效率:
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(); }
优化说明
- 移除注解逻辑:直接通过语义模型获取符号,避免注解失效问题,简化代码。
- 调整重命名顺序:先处理成员再处理类型,确保类型名称变更前,成员符号已被正确追踪和重命名。
- 批量处理:一次性收集所有需要重命名的符号,减少重复的语义分析操作,提升处理速度。
- 添加重命名判断:仅当名称需要变更时才执行重命名,避免无意义操作。
修改后运行代码,base.Channel.getDataAsync(request)会被正确重命名为base.Channel.GetDataAsync(request),所有驼峰命名的类型和成员都会转换为帕斯卡格式。
内容的提问来源于stack exchange,提问作者Marcel Lorenz
相关产品推荐
相关产品推荐

