I am working on an open source project to create UML.NET sequence diagrams that uses a javascript library called js-sequence-diagrams. I'm not sure that Roslyn is the right tool for this job, but I thought I would do it, so I put together some proof of conceptual code that tries to get all the methods and their calls, and then prints those calls in a form that can be interpreted js-sequence diagrams.
The code generates some output, but it does not capture everything. I cannot capture calls using extension methods, calls to static methods in static classes.
I see method calls with out parameters, but not in any form that extends BaseMethodDeclarationSyntax
Here is the code (keep in mind that this is proof of concept code, and therefore I did not fully adhere to the best practices, but I do not request a review of the code here ... also, I am used to using Tasks, so I was busy with the wait, but not quite sure that I use it correctly)
https://gist.github.com/SoundLogic/11193841
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection.Emit; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.FindSymbols; using System.Collections.Immutable; namespace Diagrams { class Program { static void Main(string[] args) { string solutionName = "Diagrams"; string solutionExtension = ".sln"; string solutionFileName = solutionName + solutionExtension; string rootPath = @"C:\Workspace\"; string solutionPath = rootPath + solutionName + @"\" + solutionFileName; MSBuildWorkspace workspace = MSBuildWorkspace.Create(); DiagramGenerator diagramGenerator = new DiagramGenerator( solutionPath, workspace ); diagramGenerator.ProcessSolution(); #region reference //TODO: would ReferencedSymbol.Locations be a better way of accessing MethodDeclarationSyntaxes? //INamedTypeSymbol programClass = compilation.GetTypeByMetadataName("DotNetDiagrams.Program"); //IMethodSymbol barMethod = programClass.GetMembers("Bar").First(s => s.Kind == SymbolKind.Method) as IMethodSymbol; //IMethodSymbol fooMethod = programClass.GetMembers("Foo").First(s => s.Kind == SymbolKind.Method) as IMethodSymbol; //ITypeSymbol fooSymbol = fooMethod.ContainingType; //ITypeSymbol barSymbol = barMethod.ContainingType; //Debug.Assert(barMethod != null); //Debug.Assert(fooMethod != null); //List<ReferencedSymbol> barReferencedSymbols = SymbolFinder.FindReferencesAsync(barMethod, solution).Result.ToList(); //List<ReferencedSymbol> fooReferencedSymbols = SymbolFinder.FindReferencesAsync(fooMethod, solution).Result.ToList(); //Debug.Assert(barReferencedSymbols.First().Locations.Count() == 1); //Debug.Assert(fooReferencedSymbols.First().Locations.Count() == 0); #endregion Console.ReadKey(); } } class DiagramGenerator { private Solution _solution; public DiagramGenerator( string solutionPath, MSBuildWorkspace workspace ) { _solution = workspace.OpenSolutionAsync(solutionPath).Result; } public async void ProcessSolution() { foreach (Project project in _solution.Projects) { Compilation compilation = await project.GetCompilationAsync(); ProcessCompilation(compilation); } } private async void ProcessCompilation(Compilation compilation) { var trees = compilation.SyntaxTrees; foreach (var tree in trees) { var root = await tree.GetRootAsync(); var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>(); foreach (var @class in classes) { ProcessClass( @class, compilation, tree, root ); } } } private void ProcessClass( ClassDeclarationSyntax @class , Compilation compilation , SyntaxTree tree , SyntaxNode root) { var methods = @class.DescendantNodes().OfType<MethodDeclarationSyntax>(); foreach (var method in methods) { var model = compilation.GetSemanticModel(tree); // Get MethodSymbol corresponding to method var methodSymbol = model.GetDeclaredSymbol(method); // Get all InvocationExpressionSyntax in the above code. var allInvocations = root.DescendantNodes().OfType<InvocationExpressionSyntax>(); // Use GetSymbolInfo() to find invocations of target method var matchingInvocations = allInvocations.Where(i => model.GetSymbolInfo(i).Symbol.Equals(methodSymbol)); ProcessMethod( matchingInvocations, method, @class); } var delegates = @class.DescendantNodes().OfType<DelegateDeclarationSyntax>(); foreach (var @delegate in delegates) { var model = compilation.GetSemanticModel(tree); // Get MethodSymbol corresponding to method var methodSymbol = model.GetDeclaredSymbol(@delegate); // Get all InvocationExpressionSyntax in the above code. var allInvocations = tree.GetRoot().DescendantNodes().OfType<InvocationExpressionSyntax>(); // Use GetSymbolInfo() to find invocations of target method var matchingInvocations = allInvocations.Where(i => model.GetSymbolInfo(i).Symbol.Equals(methodSymbol)); ProcessDelegates(matchingInvocations, @delegate, @class); } } private void ProcessMethod( IEnumerable<InvocationExpressionSyntax> matchingInvocations , MethodDeclarationSyntax methodDeclarationSyntax , ClassDeclarationSyntax classDeclarationSyntax ) { foreach (var invocation in matchingInvocations) { MethodDeclarationSyntax actingMethodDeclarationSyntax = null; if (SyntaxNodeHelper.TryGetParentSyntax(invocation, out actingMethodDeclarationSyntax)) { var r = methodDeclarationSyntax; var m = actingMethodDeclarationSyntax; PrintCallerInfo( invocation , classDeclarationSyntax , m.Identifier.ToFullString() , r.ReturnType.ToFullString() , r.Identifier.ToFullString() , r.ParameterList.ToFullString() , r.TypeParameterList != null ? r.TypeParameterList.ToFullString() : String.Empty ); } } } private void ProcessDelegates( IEnumerable<InvocationExpressionSyntax> matchingInvocations , DelegateDeclarationSyntax delegateDeclarationSyntax , ClassDeclarationSyntax classDeclarationSyntax ) { foreach (var invocation in matchingInvocations) { DelegateDeclarationSyntax actingMethodDeclarationSyntax = null; if (SyntaxNodeHelper.TryGetParentSyntax(invocation, out actingMethodDeclarationSyntax)) { var r = delegateDeclarationSyntax; var m = actingMethodDeclarationSyntax; PrintCallerInfo( invocation , classDeclarationSyntax , m.Identifier.ToFullString() , r.ReturnType.ToFullString() , r.Identifier.ToFullString() , r.ParameterList.ToFullString() , r.TypeParameterList != null ? r.TypeParameterList.ToFullString() : String.Empty ); } } } private void PrintCallerInfo( InvocationExpressionSyntax invocation , ClassDeclarationSyntax classBeingCalled , string callingMethodName , string returnType , string calledMethodName , string calledMethodArguments , string calledMethodTypeParameters = null ) { ClassDeclarationSyntax parentClassDeclarationSyntax = null; if (!SyntaxNodeHelper.TryGetParentSyntax(invocation, out parentClassDeclarationSyntax)) { throw new Exception(); } calledMethodTypeParameters = calledMethodTypeParameters ?? String.Empty; var actedUpon = classBeingCalled.Identifier.ValueText; var actor = parentClassDeclarationSyntax.Identifier.ValueText; var callInfo = callingMethodName + "=>" + calledMethodName + calledMethodTypeParameters + calledMethodArguments; var returnCallInfo = returnType; string info = BuildCallInfo( actor , actedUpon , callInfo , returnCallInfo); Console.Write(info); } private string BuildCallInfo(string actor, string actedUpon, string callInfo, string returnInfo) { const string calls = "->"; const string returns = "-->"; const string descriptionSeparator = ": "; string callingInfo = actor + calls + actedUpon + descriptionSeparator + callInfo; string returningInfo = actedUpon + returns + actor + descriptionSeparator + "returns " + returnInfo; callingInfo = callingInfo.RemoveNewLines(true); returningInfo = returningInfo.RemoveNewLines(true); string result = callingInfo + Environment.NewLine; result += returningInfo + Environment.NewLine; return result; } } static class SyntaxNodeHelper { public static bool TryGetParentSyntax<T>(SyntaxNode syntaxNode, out T result) where T : SyntaxNode { // set defaults result = null; if (syntaxNode == null) { return false; } try { syntaxNode = syntaxNode.Parent; if (syntaxNode == null) { return false; } if (syntaxNode.GetType() == typeof (T)) { result = syntaxNode as T; return true; } return TryGetParentSyntax<T>(syntaxNode, out result); } catch { return false; } } } public static class StringEx { public static string RemoveNewLines(this string stringWithNewLines, bool cleanWhitespace = false) { string stringWithoutNewLines = null; List<char> splitElementList = Environment.NewLine.ToCharArray().ToList(); if (cleanWhitespace) { splitElementList.AddRange(" ".ToCharArray().ToList()); } char[] splitElements = splitElementList.ToArray(); var stringElements = stringWithNewLines.Split(splitElements, StringSplitOptions.RemoveEmptyEntries); if (stringElements.Any()) { stringWithoutNewLines = stringElements.Aggregate(stringWithoutNewLines, (current, element) => current + (current == null ? element : " " + element)); } return stringWithoutNewLines ?? stringWithNewLines; } } }
Any guidance here would be much appreciated!