├── .gitignore ├── ClassDiagramGenerator.cs ├── LICENSE.md ├── Program.cs ├── README.md └── project.json /.gitignore: -------------------------------------------------------------------------------- 1 | ### Csharp ### 2 | 3 | # Build results 4 | [Dd]ebug/ 5 | [Dd]ebugPublic/ 6 | [Rr]elease/ 7 | [Rr]eleases/ 8 | x64/ 9 | x86/ 10 | bld/ 11 | [Bb]in/ 12 | [Oo]bj/ 13 | [Ll]og/ 14 | 15 | # Lock 16 | project.lock.json 17 | -------------------------------------------------------------------------------- /ClassDiagramGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using Microsoft.CodeAnalysis.CSharp.Symbols; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | using System.IO; 8 | 9 | // ClassDiagramGenerator from https://github.com/pierre3/PlantUmlClassDiagramGenerator 10 | namespace Diagrams 11 | { 12 | /// 13 | /// C#のソースコードからPlantUMLのクラス図を生成するクラス 14 | /// 15 | public class ClassDiagramGenerator : CSharpSyntaxWalker 16 | { 17 | private TextWriter writer; 18 | private string indent; 19 | private int nestingDepth = 0; 20 | 21 | /// 22 | /// Constructor 23 | /// 24 | /// 結果を出力するTextWriter 25 | /// インデントとして使用する文字列 26 | public ClassDiagramGenerator(TextWriter writer, string indent) 27 | { 28 | this.writer = writer; 29 | this.indent = indent; 30 | } 31 | 32 | /// 33 | /// インターフェースの定義をPlantUMLの書式で出力する 34 | /// 35 | public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) 36 | { 37 | VisitTypeDeclaration(node, () => base.VisitInterfaceDeclaration(node)); 38 | } 39 | 40 | /// 41 | /// クラスの定義をPlantUMLの書式で出力する 42 | /// 43 | public override void VisitClassDeclaration(ClassDeclarationSyntax node) 44 | { 45 | VisitTypeDeclaration(node, () => base.VisitClassDeclaration(node)); 46 | } 47 | 48 | /// 49 | /// 構造体の定義をPlantUMLの書式で出力する 50 | /// 51 | public override void VisitStructDeclaration(StructDeclarationSyntax node) 52 | { 53 | var name = node.Identifier.ToString(); 54 | var typeParam = node.TypeParameterList?.ToString() ?? ""; 55 | 56 | WriteLine($"class {name}{typeParam} <> {{"); 57 | 58 | nestingDepth++; 59 | base.VisitStructDeclaration(node); 60 | nestingDepth--; 61 | 62 | WriteLine("}"); 63 | } 64 | 65 | /// 66 | /// 列挙型の定義をPlantUMLの書式で出力する 67 | /// 68 | /// 69 | public override void VisitEnumDeclaration(EnumDeclarationSyntax node) 70 | { 71 | WriteLine($"{node.EnumKeyword} {node.Identifier} {{"); 72 | 73 | nestingDepth++; 74 | base.VisitEnumDeclaration(node); 75 | nestingDepth--; 76 | 77 | WriteLine("}"); 78 | } 79 | 80 | /// 81 | /// 型(クラス、インターフェース、構造体)の定義をPlantUMLの書式で出力する 82 | /// 83 | 84 | private void VisitTypeDeclaration(TypeDeclarationSyntax node, Action visitBase) 85 | { 86 | var modifiers = GetTypeModifiersText(node.Modifiers); 87 | var keyword = (node.Modifiers.Any(SyntaxKind.AbstractKeyword) ? "abstract " : "") 88 | + node.Keyword.ToString(); 89 | var name = node.Identifier.ToString(); 90 | var typeParam = node.TypeParameterList?.ToString() ?? ""; 91 | 92 | WriteLine($"{keyword} {name}{typeParam} {modifiers}{{"); 93 | 94 | nestingDepth++; 95 | visitBase(); 96 | nestingDepth--; 97 | 98 | WriteLine("}"); 99 | 100 | if (node.BaseList != null) 101 | { 102 | foreach (var b in node.BaseList.Types) 103 | { 104 | WriteLine($"{name} <|-- {b.Type.ToFullString()}"); 105 | } 106 | } 107 | } 108 | 109 | public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) 110 | { 111 | var modifiers = GetMemberModifiersText(node.Modifiers); 112 | var name = node.Identifier.ToString(); 113 | var args = node.ParameterList.Parameters.Select(p => $"{p.Identifier}:{p.Type}"); 114 | 115 | WriteLine($"{modifiers}{name}({string.Join(", ", args)})"); 116 | } 117 | /// 118 | /// フィールドの定義を出力する 119 | /// 120 | public override void VisitFieldDeclaration(FieldDeclarationSyntax node) 121 | { 122 | var modifiers = GetMemberModifiersText(node.Modifiers); 123 | var typeName = node.Declaration.Type.ToString(); 124 | var variables = node.Declaration.Variables; 125 | foreach (var field in variables) 126 | { 127 | var useLiteralInit = field.Initializer?.Value?.Kind().ToString().EndsWith("LiteralExpression") ?? false; 128 | var initValue = useLiteralInit ? (" = " + field.Initializer.Value.ToString()) : ""; 129 | 130 | WriteLine($"{modifiers}{field.Identifier} : {typeName}{initValue}"); 131 | } 132 | } 133 | 134 | /// 135 | /// プロパティの定義を出力する 136 | /// 137 | public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node) 138 | { 139 | var modifiers = GetMemberModifiersText(node.Modifiers); 140 | var name = node.Identifier.ToString(); 141 | var typeName = node.Type.ToString(); 142 | var accessor = node.AccessorList.Accessors 143 | .Where(x => !x.Modifiers.Select(y => y.Kind()).Contains(SyntaxKind.PrivateKeyword)) 144 | .Select(x => $"<<{(x.Modifiers.ToString() == "" ? "" : (x.Modifiers.ToString() + " "))}{x.Keyword}>>"); 145 | 146 | var useLiteralInit = node.Initializer?.Value?.Kind().ToString().EndsWith("LiteralExpression") ?? false; 147 | var initValue = useLiteralInit ? (" = " + node.Initializer.Value.ToString()) : ""; 148 | 149 | WriteLine($"{modifiers}{name} : {typeName} {string.Join(" ", accessor)}{initValue}"); 150 | } 151 | 152 | /// 153 | /// メソッドの定義を出力する 154 | /// 155 | public override void VisitMethodDeclaration(MethodDeclarationSyntax node) 156 | { 157 | var modifiers = GetMemberModifiersText(node.Modifiers); 158 | var name = node.Identifier.ToString(); 159 | var returnType = node.ReturnType.ToString(); 160 | var args = node.ParameterList.Parameters.Select(p => $"{p.Identifier}:{p.Type}"); 161 | 162 | WriteLine($"{modifiers}{name}({string.Join(", ", args)}) : {returnType}"); 163 | } 164 | 165 | /// 166 | /// 列挙型のメンバーを出力する 167 | /// 168 | /// 169 | public override void VisitEnumMemberDeclaration(EnumMemberDeclarationSyntax node) 170 | { 171 | WriteLine($"{node.Identifier}{node.EqualsValue},"); 172 | } 173 | 174 | /// 175 | /// 結果出力用TextWriterに、1行書き込む。 176 | /// 177 | private void WriteLine(string line) 178 | { 179 | //行の先頭にネストの階層分だけインデントを付加する 180 | var space = string.Concat(Enumerable.Repeat(indent, nestingDepth)); 181 | writer.WriteLine(space + line); 182 | } 183 | 184 | /// 185 | /// 型(クラス、インターフェース、構造体)の修飾子を文字列に変換する 186 | /// 187 | /// 修飾子のTokenList 188 | /// 変換後の文字列 189 | private string GetTypeModifiersText(SyntaxTokenList modifiers) 190 | { 191 | var tokens = modifiers.Select(token => 192 | { 193 | switch (token.Kind()) 194 | { 195 | case SyntaxKind.PublicKeyword: 196 | case SyntaxKind.PrivateKeyword: 197 | case SyntaxKind.ProtectedKeyword: 198 | case SyntaxKind.InternalKeyword: 199 | case SyntaxKind.AbstractKeyword: 200 | return ""; 201 | default: 202 | return $"<<{token.ValueText}>>"; 203 | } 204 | }).Where(token => token != ""); 205 | 206 | var result = string.Join(" ", tokens); 207 | if (result != string.Empty) 208 | { 209 | result += " "; 210 | }; 211 | return result; 212 | } 213 | 214 | /// 215 | /// 型のメンバーの修飾子を文字列に変換する 216 | /// 217 | /// 修飾子のTokenList 218 | /// 219 | private string GetMemberModifiersText(SyntaxTokenList modifiers) 220 | { 221 | var tokens = modifiers.Select(token => 222 | { 223 | switch (token.Kind()) 224 | { 225 | case SyntaxKind.PublicKeyword: 226 | return "+"; 227 | case SyntaxKind.PrivateKeyword: 228 | return "-"; 229 | case SyntaxKind.ProtectedKeyword: 230 | return "#"; 231 | case SyntaxKind.AbstractKeyword: 232 | case SyntaxKind.StaticKeyword: 233 | return $"{{{token.ValueText}}}"; 234 | case SyntaxKind.InternalKeyword: 235 | default: 236 | return $"<<{token.ValueText}>>"; 237 | } 238 | }); 239 | 240 | var result = string.Join(" ", tokens); 241 | if (result != string.Empty) 242 | { 243 | result += " "; 244 | }; 245 | return result; 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Gabriele Tomassetti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.CodeAnalysis.CSharp; 5 | using System.IO; 6 | using Microsoft.CodeAnalysis.Text; 7 | using System.Text; 8 | 9 | namespace Diagrams 10 | { 11 | class Program 12 | { 13 | static void Main(string[] args) 14 | { 15 | Console.WriteLine("****************************************************************************"); 16 | Console.WriteLine("* *"); 17 | Console.WriteLine("* Diagram generator for PlantUML *"); 18 | Console.WriteLine("* To generata a class diagram specify a file .cs or a directory *"); 19 | Console.WriteLine("* *"); 20 | Console.WriteLine("****************************************************************************"); 21 | 22 | // to test Class Diagram 23 | // args = new[] { @"ClassDiagramGenerator.cs", @"..\uml" }; 24 | 25 | if (args.Length < 1) 26 | { 27 | Console.WriteLine("Specify a file or directory"); 28 | return; 29 | } 30 | 31 | var input = args[0]; 32 | 33 | IEnumerable files; 34 | if (Directory.Exists(input)) 35 | { 36 | files = Directory.EnumerateFiles(Path.GetFullPath(input), "*.cs"); 37 | } 38 | else if (File.Exists(input)) 39 | { 40 | try 41 | { 42 | var fullname = Path.GetFullPath(input); 43 | files = new[] { fullname }; 44 | } 45 | catch 46 | { 47 | Console.WriteLine("Invalid name"); 48 | return; 49 | } 50 | } 51 | else 52 | { 53 | Console.WriteLine("Specify an existing file or directory"); 54 | return; 55 | } 56 | 57 | var outputDir = ""; 58 | if (args.Length >= 2) 59 | { 60 | if (Directory.Exists(args[1])) 61 | { 62 | outputDir = args[1]; 63 | } 64 | } 65 | 66 | if (outputDir == "") 67 | { 68 | outputDir = Path.Combine(Path.GetDirectoryName(files.First()), "uml"); 69 | Directory.CreateDirectory(outputDir); 70 | } 71 | 72 | foreach (var file in files) 73 | { 74 | Console.WriteLine($"Generation PlantUML text for {file}..."); 75 | string outputFile = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(file)); 76 | 77 | using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read)) 78 | { 79 | var tree = CSharpSyntaxTree.ParseText(SourceText.From(stream)); 80 | var root = tree.GetRoot(); 81 | 82 | using (var writer = new StreamWriter(new FileStream(outputFile + ".ClassDiagram.plantuml", FileMode.OpenOrCreate, FileAccess.Write))) 83 | { 84 | writer.WriteLine("@startuml"); 85 | 86 | var gen = new ClassDiagramGenerator(writer, " "); 87 | gen.Visit(root); 88 | 89 | writer.Write("@enduml"); 90 | } 91 | } 92 | } 93 | 94 | Console.WriteLine("Completed"); 95 | } 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generate diagrams from source code 2 | 3 | This is an example on how to use Roslyn to support other parts of your job. Specifically, it can be used to create a text representation of a class diagram from your source code. This representation can then be used by PlantUML to create an image. The advantage of this approach is that the class diagrams can be generated programmatically and can be integrated in the systems of continuous integration that you already use (or your git/mercurial repository). 4 | 5 | **You can read an article on the example on [Generate diagrams from C# source code](http://tomassetti.me/generate-diagrams-csharp/)** 6 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "debugType": "portable", 5 | "emitEntryPoint": true 6 | }, 7 | "dependencies": { 8 | "Microsoft.Net.Compilers" : "1.3.2", 9 | "Microsoft.CodeAnalysis" : "1.3.2" 10 | }, 11 | "frameworks": { 12 | "netcoreapp1.0": { 13 | "dependencies": { 14 | "Microsoft.NETCore.App": { 15 | "type": "platform", 16 | "version": "1.0.1" 17 | } 18 | }, 19 | "imports": ["dnxcore50", "portable-net45+win8+wp8+wpa81"] 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------