我有一个小型 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);
}
}
所需包裹:
Microsoft.CodeAnalysis.CSharp
Microsoft.CodeAnalysis.CSharp.Workspaces
您会看到,在结果代码中
base.Channel.getDataAsync(request)
没有被重命名。我可以保证 base.Channel
属于类型 Test.web
。
我找不到关于 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);
}