Autor Beitrag
Kasko
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 126
Erhaltene Danke: 1

Win 10
C# C++ (VS 2017/19), (Java, PHP)
BeitragVerfasst: Mo 23.10.23 13:29 
Ich schreibe ein kleines CLI Tool, welches den Code, welcher von einem "VS Connected-Service" erstellt wird, aufteilt und auf verschiedene Projekte in der Solution verteilt (Trennung von Models, Service-Definitionen und Service-Implementierungen). Leider sind die Messages und Operations innerhalb der WSDL Dokumentation in camelCase benannt, was dafür sorgt, dass auch die Models und Service/Client-Methoden in camelCase benannt werden. Um das zu verhindern möchte ich die Models und Client-Methoden in PascalCase umbenennen. Dafür nutze ich den Microsoft.CodeAnalysis.Rename.Renamer. Der Code ist bei 300 Operations bemerkenswert langsam, aber benennt alle Deklarationen korrekt um. Jedoch werden alle Referenzen, wie Methoden- und Property-Aufrufe nicht umbenannt, wodurch der erstellte Code vor Fehlern nur so strotzt.

Hier ist ein Codebeispiel mit einem Minimal reproducible example:

ausblenden volle Höhe C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

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 {
                    string data { get; }
                    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 string data => base.Channel.data;

                    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 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 document.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();
                    document = (await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, options, newName)).Projects.First().Documents.First();
                }
            }
        }

        var newSourceCode = (await document!.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);
    }
}


Notwendige NUGET-Packages:
1. Microsoft.CodeAnalysis.CSharp
2. Microsoft.CodeAnalysis.CSharp.Workspaces

Nach Ausführen des Codes wird alles umbenannt mit Ausnahme von base.Channel.getDataAsync(request) und base.Channel.data. Des weiteren werden für das umbenannte Symbol VOR DER UMBENENNUNG über SymbolFinder.FindReferencesAsync keine Locations gefunden.

Kann mir jemand erklären, warum die Aufrufe nicht umbenannt werden und wie ich meinen Code anpassen muss um dies zu erreichen?
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4764
Erhaltene Danke: 1052

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: Di 24.10.23 16:36 
Hallo,

das ist schon ein sehr spezielles Thema (mit dem ich mich bisher auch nicht beschäftigt habe).

Was mir aber auffällt, ist, daß du nur MethodDeclarationSyntax verwendest, das scheint für mich nur die Deklaration einer Methode zu sein (also Methodensignatur) und nicht die Aufrufe von Methoden - diese sollten Teil eines Ausdrucks (engl. expression) sein. Gefunden habe ich dazu ExpressionStatementSyntax - vllt. hilft dir das weiter?!
Kasko Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 126
Erhaltene Danke: 1

Win 10
C# C++ (VS 2017/19), (Java, PHP)
BeitragVerfasst: Di 24.10.23 18:51 
Hi,

ich greife lediglich auf die Deklarationen von Typen sowie von ausgewählten Membern (Methoden und Properties) zu, da das SemanticModel mir lediglich für Deklarationen die jeweiligen Symbole liefert (GetDeclaredSymbol). Dieses Symbol und entsprechend alle Referenzen sollen dann umbenannt werden. Bei Typen funktioniert das einwandfrei, da wie im Ergebnis zu sehen z.B. die Symbol-Referenz Test.Web innerhalb der Liste der generischen Typparameter von System.ServiceModel.ClientBase zusätzlich zur Deklaration umbenannt wird. Diese Referenz ist innerhalb des Syntax-Trees lediglich ein QualifiedName.

Bei Methoden und Properties funktioniert das jedoch nicht. Die Symbol-Referenzen in Form von MemberAccessExpressionSyntax werden nicht umbenannt. Siehe vollständigen Syntax-Tree auf Sharplab.io. Ich kann auch nicht einfach alle MemberAccessExpressionSyntax umbenennen, da dies auch Felder mit einschließt. Daher bin ich auf Roslyn und den mitgelieferten Renamer bzw. auf die Symbol-Referenzen des semantischen Modells angewiesen.
Th69
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Moderator
Beiträge: 4764
Erhaltene Danke: 1052

Win10
C#, C++ (VS 2017/19/22)
BeitragVerfasst: Di 24.10.23 21:28 
Dann mußt du halt eine Liste halten, welche Identifier du umbenennen willst und diese dann entsprechend vergleichen, bevor du diesen ersetzt.

PS: Bzgl. Performance ist mir an deinem Code auch aufgefallen, daß du mehrmals pro Schleife typeDeclaration.DescendantNodes() aufrufst. Und ToList() ist bei AddRanges(...) auch unnötig, da es IEnumerable<T> entgegennimmt.
Und warum rufst du semanticModel = await document.GetSemanticModelAsync() sowie await semanticModel!.SyntaxTree.GetRootAsync() innerhalb der Schleife auf, anstatt nur einmalig davor?!
Kasko Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 126
Erhaltene Danke: 1

Win 10
C# C++ (VS 2017/19), (Java, PHP)
BeitragVerfasst: Mi 25.10.23 13:13 
1. Liste von Identifiern:

Das ist so nicht möglich. Der Name alleine macht einen Identifier ja nicht eindeutig. Daher würde ich auch alle gleichnamigen Identifier für Felder, Constructor- oder Methoden-Parameter sowie lokale Variablen umbenennen. Daher bin ich auf das Ergebnis der semantischen Analyse angewiesen. Bei so aussagekräftigen Identifiern wie message würde das komplett nach hinten losgehen.

2. Performance

Ja das ToList ist unnötig, hat aber keine signifikante Auswirkung auf die Performance. Bezüglich dem Rest: Sogut wie alle Strukturen der Roslyn API sind immutable. Solution, Project, Document und auch der Syntax Tree. Jeder Umbenennung durch Renamer.RenameSymbolAsync oder eine Änderung am SyntaxTree erstellt diese Strukur neu. Daher muss auch das semantische Model immer neu erstellt werden. Dies ist auch der Grund, weshalb ich vor Erstellung des AdhocWorkspace den SyntaxTree mit Annotations präpariere, um die Nodes innerhalb eines neu erstellten SystaxTrees wiederzufinden. Ich benötige diese neuen Nodes. Andernfalls würde ich je nach Aktion eine dieser beiden Exceptions erhalten:

1. System.InvalidOperationException: Operation is not valid due to the current state of the object.
2. System.ArgumentException: Syntax node is not within syntax tree

3. Lösung

Die semantische Analyse konnte aufgrund fehlender Metadaten nicht abgeschlossen werden. Habe also die entsprechenden DLLs der Nuget-Packages als Metadaten-Referenzen zum Projekt hinzugefügt. Dadurch konnte die Analyse abgeschlossen werden. Vorher lagen für base.Channel.getDataAsync keine Symbol-Informationen vor:

ausblenden C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
var project = new AdhocWorkspace().CurrentSolution.AddProject("TempProject""TempAssembly", LanguageNames.CSharp)
            .AddMetadataReferences(GetAssemblyReferences());

private static IEnumerable<MetadataReference> GetAssemblyReferences()
    {
        var outputDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        var assembliesInOutput = Directory.GetFiles(outputDirectory, "*.dll");
        return assembliesInOutput
            .Select(assemblyPath => MetadataReference.CreateFromFile(assemblyPath))
            .ToList();
    }

Für diesen Beitrag haben gedankt: Th69