LoginSignup
6
8

More than 3 years have passed since last update.

C# ソースコードのフローチャートを生成する(2)

Posted at

はじめに

前回ソースコードのフローチャートを作成するところまで書きました。
今回トレースするところまで実装します。
頻繁に使うことは無いと思われるので全自動化するのは最初から断念しています。
以下のような手順でトレースを行います。
手順.png

トレース用のソースコードを作成する

変換前ソース

以下のソースコードで試してみます。

Source.cs
public class Source
{
    public bool Working { get; set; } = true;
    public int Value { get; set; } = 0;
    public void DoJob()
    {
        var t = Task.Run(async () =>
        {
            //稼動中繰り返す
            while(Working)
            {
                //値を判断
                switch (Value)
                {
                    case 0:
                        //処理開始
                        System.Console.WriteLine("処理開始");
                        break;
                    case 1:
                        //更新開始
                        System.Console.WriteLine("更新開始");
                        break;
                    case 6:
                        System.Console.WriteLine("ここにはこない");
                        break;
                    default:
                        //値が上限まで達したか
                        if(Value >= 5)
                        {
                            //更新終了
                            System.Console.WriteLine("更新終了");
                            //処理終了とする
                            Working = false;
                        }
                        break;
                }
                await NewValue();
                await Task.Delay(500);
            }                
        });
        t.Wait();
    }

    private async Task NewValue()
    {
        Command.ReqValue(Value);
        await Task.Delay(1000);
        Value++;            
    }
}

変換後(トレース用)ソース

前回も書きましたが、これをトレースするにはトレース用のコードを埋め込みます。

  • FlowTrace はトレース用のクラスです。
  • FlowTrace#InitGraphは初期状態のフローを作成します。
  • FlowTrace#TraceNodeは通過したノードを記憶します。
  • 処理開始時("node1"の個所)TraceNodeの第2引数をtrueにしていますが、これにより関数間のエッジを作成しません。
    デフォルト(false)だと関数間のエッジを作成しますが、共通関数などあっちこっちからエッジが繋がってしまい見にくくなります。
  • whileなどのループする構文("node3"の個所)はループに入らないこともあるので、ループに入る前と入った直後の2か所にトレースを埋め込みます。
  • Mainは手書きで追加しています。
  • 今回の実装ではendターミナル("node18")のトレースを埋め込んでいません。
    必要であれば手で埋め込みます。
ConvSource.cs
public class ConvSource
{
    public bool Working { get; set; } = true;
    public int Value { get; set; } = 0;

    //手書き--ここから
    //args[0]:dotファイル(トレース結果)出力先
    static void Main(string[] args)
    {
        var conv = new ConvSource();
        conv.InitGraph();
        conv.DoJob();
        conv.Trace.WriteDot(args[0]);
    }
    //手書き--ここまで

    public void DoJob()
    {
        Trace.TraceNode("node1", true);
        Trace.TraceNode("node2");
        var t = Task.Run(async () =>
        {
            //稼動中繰り返す
            Trace.TraceNode("node3");
            while (Working)
            {
                //値を判断
                Trace.TraceNode("node3");
                Trace.TraceNode("node4");
                switch (Value)
                //省略
    }

    public FlowTrace Trace = new FlowTrace();
    public void InitGraph()
    {
        Trace.AddFunc(0);
        Trace.AddNode(0, "node1", "ellipse", "DoJob\nstart");
        //省略
        Trace.AddEdge("node1", "node2", "");
        Trace.AddEdge("node4", "node5", "case 0:");
        //省略    
  }

出力結果

赤が通過個所、括弧内は回数を示します。
初期フローは非同期処理に対応していないので、トレースすることにより想定外のエッジ(「Unintended」と表記)が追加されることがあります。
このサンプルの場合、Task.Run()のラムダ式が実行される前にt.Wait()が実行されているみたいです。このような動きになるとは知りませんでした。
syntax3.txt.png

変換プログラムのソースコード

Dotファイル出力およびソースコード変換

前回掲載したコード(FlowGraph.cs)にMain処理を統合し、トレース用ソースコードの変換処理を追加しています。
変更が無い部分(CreateGraphNode以降)は省略しています。
またSyntaxVisitorも同様に省略しています。(一部修正しています)

FlowGraph.cs
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using System.IO;

namespace Flow
{
    public class FlowGraph
    {
        private static int WORD_WRAP_LENGTH = 40;
        private List<GraphEdge> EdgeList = new List<GraphEdge>();
        private Dictionary<SyntaxNode, GraphNode> NodeMap = new System.Collections.Generic.Dictionary<SyntaxNode, GraphNode>();
        private IEnumerable<BaseMethodDeclarationSyntax> memberDeclarations;
        private string _sourceCode;
        private int _seq = 0;

        //args[0]:対象c#ソースファイル
        //args[1]:dotファイル(静的フローチャート)出力先
        //args[2]:変換後ソース(トレース用コード)出力先
        static void Main(string[] args)
        {
            string sourceCode = File.ReadAllText(args[0]);
            FlowGraph graph = new FlowGraph(sourceCode);
            //静的フロー出力
            graph.WriteDot(args[1]);
            //ソースコード変換
            graph.ConvSource(args[2]);
        }

        public FlowGraph(string sourceCode)
        {
            _sourceCode = sourceCode;
            var tree = CSharpSyntaxTree.ParseText(_sourceCode);
            memberDeclarations = tree.GetRoot().DescendantNodes().OfType<BaseMethodDeclarationSyntax>();
            if(memberDeclarations.Count() == 0)
            {
                return;
            }
            foreach (var funcRoot in memberDeclarations)
            {
                SyntaxVisitor ast = new SyntaxVisitor(this);
                if (funcRoot is MethodDeclarationSyntax || funcRoot is ConstructorDeclarationSyntax)
                    ast.VisitRootSyntax(funcRoot);
            }
        }

        //トレース用のソースコード出力
        public void ConvSource(string outFile)
        {
            var result = _sourceCode;
            var keys = new List<SyntaxNode>(NodeMap.Keys).OrderByDescending(x => x.Span.Start);
            AddInitStatements(ref result, memberDeclarations.Last());
            foreach (var sNode in keys)
            {
                AddTraceStatement(ref result, NodeMap[sNode], sNode);
            }
            using (System.IO.StreamWriter writer = new System.IO.StreamWriter(outFile, false))
            {
                writer.Write(result);
            }
        }

        //ノード、エッジ登録関数部作成
        private void AddInitStatements(ref string result, SyntaxNode lastMetod)
        {
            var clustermap = new Dictionary<SyntaxNode, int>();
            int ipoint = lastMetod.FullSpan.End;
            string spaces = new string(' ', lastMetod.GetLocation().GetLineSpan().StartLinePosition.Character);
            var sb = new StringBuilder();
            sb.Append("\r\n" + spaces + "public FlowTrace Trace = new FlowTrace();\r\n");
            sb.Append(spaces + "public void InitGraph()\r\n");
            sb.Append(spaces + "{\r\n");
            //クラスタIDを追加する
            string spaces2 = new string(' ', spaces.Length + 4);
            int cluster = 0;
            foreach (var funcRoot in memberDeclarations)
            {
                sb.Append(spaces2 + "Trace.AddFunc(" + cluster + ");\r\n");
                foreach (SyntaxNode sNode in NodeMap.Keys)
                {
                    if (sNode == funcRoot || sNode.Ancestors(true).Contains(funcRoot))
                    {
                        var gNode = NodeMap[sNode];
                        string isFunc = sNode == funcRoot ? "true" : "false";
                        string label = gNode.LabelName.Replace("\r", "").Replace("\n", "\\n").Replace("\"", "\\\"");
                        sb.Append(spaces2 + "Trace.AddNode(" + cluster + ", \"" + gNode.NodeId + "\", \"" + gNode.NodeShape + "\", \"" + label + "\"" + ");\r\n");
                    }
                }
                cluster++;
            }

            foreach (GraphEdge gEdge in EdgeList)
            {
                string label = gEdge.CenterLabel.Replace("\r", "").Replace("\n", "\\n").Replace("\"", "\\\"");
                sb.Append(spaces2 + "Trace.AddEdge(\"" + gEdge.TailNode.NodeId + "\", \"" + gEdge.HeadNode.NodeId + "\", \"" + label + "\");\r\n");
            }

            sb.Append(spaces + "}\r\n");
            result = result.Insert(ipoint, sb.ToString());
        }

        //トレースコード埋め込み
        private void AddTraceStatement(ref string result, GraphNode gNode, SyntaxNode sNode)
        {
            string traceStr = $"Trace.TraceNode(\"{gNode.NodeId}\");";
            var parent = sNode.Parent;
            if (sNode is BlockSyntax)
            {
                return;
            }
            else if (sNode is MethodDeclarationSyntax ||
                sNode is ConstructorDeclarationSyntax ||
                sNode is LocalFunctionStatementSyntax)
            {
                var property = sNode.GetType().GetProperty("Body");
                var block = (BlockSyntax)property.GetValue(sNode);
                string spaces = new string(' ', block.GetLocation().GetLineSpan().StartLinePosition.Character + 4);
                result = result.Insert(block.Span.Start + 1, "\r\n" + spaces + $"Trace.TraceNode(\"{gNode.NodeId}\", true);");
                return;
            }
            else if (parent is ElseClauseSyntax || parent is IfStatementSyntax)
            {
                var property = sNode.Parent.GetType().GetProperty("Statement");
                var statement = property.GetValue(sNode.Parent);
                string spaces = new string(' ', parent.GetLocation().GetLineSpan().StartLinePosition.Character);
                result = result.Insert(sNode.Span.End, "\r\n" + spaces + "}");
                string spaces2 = new string(' ', sNode.GetLocation().GetLineSpan().StartLinePosition.Character);
                result = result.Insert(((StatementSyntax)statement).Span.Start, "{" + "\r\n" + spaces2 + traceStr + "\r\n" + spaces2);
                return;
            }
            else if (sNode is ExpressionSyntax expr)
            {
                if (expr.Parent is DoStatementSyntax doSntax2)
                {
                    try
                    {
                        var lastStatement = doSntax2.Statement.ChildNodes().Last();
                        string spaces = new string(' ', lastStatement.GetLocation().GetLineSpan().StartLinePosition.Character);
                        result = result.Insert(lastStatement.Span.End, "\r\n" + spaces + traceStr);
                    }
                    catch { }
                }
                return;
            }
            else if (sNode is DoStatementSyntax ||
                sNode is WhileStatementSyntax ||
                sNode is ForEachStatementSyntax ||
                sNode is ForStatementSyntax)
            {
                var property = sNode.GetType().GetProperty("Statement");
                StatementSyntax statement = (StatementSyntax)property.GetValue(sNode);
                try
                {
                    var firstStatement = statement.ChildNodes().First();
                    string spaces = new string(' ', firstStatement.GetLocation().GetLineSpan().StartLinePosition.Character);
                    result = result.Insert(firstStatement.Span.Start, traceStr + "\r\n" + spaces);
                }
                catch { }
            }
            string leftPadding = new string(' ', sNode.GetLocation().GetLineSpan().StartLinePosition.Character);
            result = result.Insert(sNode.Span.Start, traceStr + "\r\n" + leftPadding);
        }

        public GraphNode CreateGraphNode(SyntaxNode astNode, string labelName, string nodeShape)
        //以下省略
    }
}

トレース用クラス

Dotファイル出力部などFlowGraphとの重複が多いですが前回のコードの見直しは最小限としました。

FlowTrace.cs
using System.Collections.Generic;
using System.Linq;

namespace Flow
{
    public class FlowTrace
    {
        private Dictionary<string, Node> nodeMap = new Dictionary<string, Node>();
        private Dictionary<(string tailId,string headId),Edge> edgeMap = new Dictionary<(string tailId, string headId), Edge>();
        private Dictionary<int, string> funcMap = new Dictionary<int, string>();

        public void AddNode(int funcId, string id, string shape, string label)
        {
            nodeMap[id] = new Node {FuncId = funcId, Id = id, Shape = shape, Label = label };
        }

        public void AddEdge(string tailId,string headId,string label)
        {
            edgeMap[(tailId, headId)] = new Edge {TailId = tailId,HeadId = headId , Label = label };
        }

        public void AddFunc(int id)
        {
            funcMap[id] = string.Empty;
        }
        public void TraceNode(string nodeId, bool reset = false)
        {
            if (nodeMap.ContainsKey(nodeId))
            {
                var node = nodeMap[nodeId];
                if(funcMap[node.FuncId] != nodeId)
                {
                    nodeMap[nodeId].PassCount++;
                    if (reset)
                    {
                        funcMap[node.FuncId] = string.Empty;
                    }
                    if (funcMap[node.FuncId] != string.Empty)
                    {
                        var edgeKey = (funcMap[node.FuncId], nodeId);
                        if (!edgeMap.ContainsKey(edgeKey))
                        {
                            edgeMap[edgeKey] = new Edge { TailId = funcMap[node.FuncId], HeadId = nodeId, Label = "Unintended", LineStyle = "dashed" };
                        }
                        edgeMap[edgeKey].PassCount++;
                    }
                    funcMap[node.FuncId] = nodeId;
                }                
            }
        }

        public void WriteDot(string dotFilename)
        {
            using (System.IO.StreamWriter writer = new System.IO.StreamWriter(dotFilename, false))
            {
                writer.WriteLine("digraph G{");
                writer.WriteLine("rankdir=TB;");
                writer.WriteLine("node[fontname = \"MS GOTHIC\"]");
                writer.WriteLine("edge[fontname = \"MS GOTHIC\"]");
                var clusters = new List<int> { -1 };
                clusters.InsertRange(0, funcMap.Keys);
                foreach (var cluster in clusters)
                {
                    if(cluster != -1)
                    {
                        writer.WriteLine("subgraph cluster_" + cluster + "{");
                    }                    
                    foreach (Node node in nodeMap.Values.OrderBy((x) => x.Id))
                    {                       
                        if (node.FuncId == cluster)
                        {
                            string label = node.Label.Replace("\n", "\\n").Replace("\"", "\\\"");
                            string countStr = string.Empty;
                            string colorStr = string.Empty;
                            if (node.PassCount > 0)
                            {
                                countStr = "(" + node.PassCount + ")";
                                colorStr = ", color=\"red\"";
                            }
                            writer.WriteLine("\"" + node.Id + "\"" + " [shape = \"" + node.Shape + "\""
                                      + colorStr + ", label = \"" + label + countStr + "\"]");
                        }
                    }
                    if (cluster != -1)
                    {
                        writer.WriteLine("}");
                    }
                }


                foreach (Edge edge in edgeMap.Values.OrderBy((x) => x.TailId))
                {
                    string countStr = string.Empty;
                    string colorStr = string.Empty;
                    if (edge.PassCount > 0)
                    {
                        countStr = "(" + edge.PassCount + ")";
                        colorStr = ", color=\"red\"";
                    }
                        writer.WriteLine("\"" + edge.TailId + "\"  -> \"" + edge.HeadId + "\""
                         + " [label =\"" + edge.Label + countStr + "\"" + colorStr + "]");
                }
                writer.WriteLine("}");
            }
        }

        private class Node
        {
            public int FuncId { get; set; }
            public string Id {get;set;}
            public string Shape { get; set; }
            public string Label { get; set; }
            public long PassCount { get; set; } = 0;
        }

        private class Edge
        {
            public string TailId { get; set; }
            public string HeadId { get; set; }
            public string Label { get; set; }
            public long PassCount { get; set; }
            public string LineStyle { get; set; } = string.Empty;
        }
    }
}
6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8