1 /** 2 Copyright: Copyright (c) 2016-2017, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 Generate PlantUML diagrams of C/C++ source code. 11 */ 12 module dextool.plugin.frontend.plantuml; 13 14 import std.typecons : Flag, Yes, No, Tuple; 15 import logger = std.experimental.logger; 16 17 import dextool.compilation_db; 18 import dextool.type; 19 20 import dextool.plugin.backend.plantuml : Controller, Parameters, Products; 21 import cpptooling.data : CppRoot, CppNamespace, CppClass; 22 23 struct RawConfiguration { 24 string[] cflags; 25 string[] compileDb; 26 string[] fileExclude; 27 string[] fileInclude; 28 string[] inFiles; 29 string componentStrip; 30 string filePrefix = "view_"; 31 string out_; 32 bool classInheritDep; 33 bool classMemberDep; 34 bool classMethod; 35 bool classParamDep; 36 bool componentByFile; 37 bool generateDot; 38 bool generateStyleInclude; 39 bool help; 40 bool shortPluginHelp; 41 bool skipFileError; 42 43 string[] originalFlags; 44 45 void parse(string[] args) { 46 import std.getopt; 47 48 originalFlags = args.dup; 49 50 // dfmt off 51 try { 52 getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help, 53 "class-method", &classMethod, 54 "class-paramdep", &classParamDep, 55 "class-inheritdep", &classInheritDep, 56 "class-memberdep", &classMemberDep, 57 "compile-db", &compileDb, 58 "comp-by-file", &componentByFile, 59 "comp-strip", &componentStrip, 60 "file-exclude", &fileExclude, 61 "file-prefix", &filePrefix, 62 "file-include", &fileInclude, 63 "gen-dot", &generateDot, 64 "gen-style-incl", &generateStyleInclude, 65 "in", &inFiles, 66 "out", &out_, 67 "short-plugin-help", &shortPluginHelp, 68 "skip-file-error", &skipFileError, 69 ); 70 } 71 catch (std.getopt.GetOptException ex) { 72 logger.error(ex.msg); 73 help = true; 74 } 75 // dfmt on 76 77 import std.algorithm : find; 78 import std.array : array; 79 import std.range : drop; 80 81 // at this point args contain "what is left". What is interesting then is those after "--". 82 cflags = args.find("--").drop(1).array(); 83 } 84 85 void printHelp() { 86 import std.stdio : writefln; 87 88 writefln("%s\n\n%s\n%s", plantuml_opt.usage, plantuml_opt.optional, plantuml_opt.others); 89 } 90 91 void dump() { 92 logger.trace(this); 93 } 94 } 95 96 // dfmt off 97 static auto plantuml_opt = Tuple!(string, "usage", string, "optional", string, "others")( 98 "usage: 99 dextool uml [options] [--compile-db=...] [--file-exclude=...] [--in=...] [--] [CFLAGS...] 100 dextool uml [options] [--compile-db=...] [--file-include=...] [--in=...] [--] [CFLAGS...]", 101 // ------------- 102 " --out=dir directory for generated files [default: ./] 103 --file-prefix=p Prefix used when generating test artifacts [default: view_] 104 --class-method Include methods in the generated class diagram 105 --class-paramdep Class method parameters as directed association in diagram 106 --class-inheritdep Class inheritance in diagram 107 --class-memberdep Class member as composition/aggregation in diagram 108 --comp-by-file Components by file instead of directory 109 --comp-strip=r Regex used to strip path used to derive component name 110 --gen-style-incl Generate a style file and include in all diagrams 111 --gen-dot Generate a dot graph block in the plantuml output 112 --skip-file-error Skip files that result in compile errors (only when using compile-db and processing all files)", 113 // ------------- 114 "others: 115 --in= Input files to parse 116 --compile-db=j Retrieve compilation parameters from the file 117 --file-exclude= Exclude files from generation matching the regex 118 --file-include= Include the scope of the test double to those files 119 matching the regex 120 121 REGEX 122 The regex syntax is found at http://dlang.org/phobos/std_regex.html 123 124 Information about --file-exclude. 125 The regex must fully match the filename the AST node is located in. 126 If it matches all data from the file is excluded from the generated code. 127 128 Information about --file-include. 129 The regex must fully match the filename the AST node is located in. 130 Only symbols from files matching the include affect the generated test double. 131 " 132 ); 133 // dfmt on 134 135 /** Frontend for PlantUML generator. 136 * 137 * TODO implement --in=... for multi-file handling 138 */ 139 class PlantUMLFrontend : Controller, Parameters, Products { 140 import std..string : toLower; 141 import std.regex : regex, Regex; 142 import std.typecons : Flag, Yes, No; 143 import my.filter : ReFilter; 144 import cpptooling.type : FilePrefix; 145 import dextool.type : Path; 146 147 import dsrcgen.plantuml; 148 149 static struct FileData { 150 import dextool.io : WriteStrategy; 151 152 Path filename; 153 string data; 154 WriteStrategy strategy; 155 } 156 157 static const fileExt = ".pu"; 158 static const inclExt = ".iuml"; 159 160 // TODO ugly hack to remove immutable. Fix it appropriately 161 Path[] input_files; 162 immutable Path output_dir; 163 immutable Path file_classes; 164 immutable Path file_components; 165 immutable Path file_style; 166 immutable Path file_style_output; 167 168 immutable FilePrefix file_prefix; 169 170 immutable Flag!"genClassMethod" gen_class_method; 171 immutable Flag!"genClassParamDependency" gen_class_param_dep; 172 immutable Flag!"genClassInheritDependency" gen_class_inherit_dep; 173 immutable Flag!"genClassMemberDependency" gen_class_member_dep; 174 immutable Flag!"doStyleIncl" do_style_incl; 175 immutable Flag!"doGenDot" do_gen_dot; 176 immutable Flag!"doComponentByFile" do_comp_by_file; 177 178 ReFilter fileFilter; 179 Regex!char comp_strip; 180 181 /// Data produced by the generator intended to be written to specified file. 182 FileData[] fileData; 183 184 static auto makeVariant(ref RawConfiguration parsed) { 185 auto gen_class_method = cast(Flag!"genClassMethod") parsed.classMethod; 186 auto gen_class_param_dep = cast(Flag!"genClassParamDependency") parsed.classParamDep; 187 auto gen_class_inherit_dep = cast(Flag!"genClassInheritDependency") parsed.classInheritDep; 188 auto gen_class_member_dep = cast(Flag!"genClassMemberDependency") parsed.classMemberDep; 189 190 auto gen_style_incl = cast(Flag!"doStyleIncl") parsed.generateStyleInclude; 191 auto gen_dot = cast(Flag!"doGenDot") parsed.generateDot; 192 auto do_comp_by_file = cast(Flag!"doComponentByFile") parsed.componentByFile; 193 194 auto variant = new PlantUMLFrontend(FilePrefix(parsed.filePrefix), 195 Path(parsed.out_), gen_style_incl, gen_dot, gen_class_method, 196 gen_class_param_dep, gen_class_inherit_dep, gen_class_member_dep, do_comp_by_file); 197 198 variant.fileFilter = ReFilter(parsed.fileInclude, parsed.fileExclude); 199 variant.comp_strip = () { 200 if (parsed.componentStrip.length != 0) 201 return regex(parsed.componentStrip); 202 return Regex!char.init; 203 }(); 204 205 return variant; 206 } 207 208 this(FilePrefix file_prefix, Path output_dir, Flag!"doStyleIncl" style_incl, 209 Flag!"doGenDot" gen_dot, Flag!"genClassMethod" class_method, 210 Flag!"genClassParamDependency" class_param_dep, Flag!"genClassInheritDependency" class_inherit_dep, 211 Flag!"genClassMemberDependency" class_member_dep, 212 Flag!"doComponentByFile" do_comp_by_file) { 213 this.file_prefix = file_prefix; 214 this.output_dir = output_dir; 215 this.gen_class_method = class_method; 216 this.gen_class_param_dep = class_param_dep; 217 this.gen_class_inherit_dep = class_inherit_dep; 218 this.gen_class_member_dep = class_member_dep; 219 this.do_comp_by_file = do_comp_by_file; 220 this.do_gen_dot = gen_dot; 221 this.do_style_incl = style_incl; 222 223 import std.path : baseName, buildPath, relativePath, stripExtension; 224 225 this.file_classes = Path(buildPath(cast(string) output_dir, 226 cast(string) file_prefix ~ "classes" ~ fileExt)); 227 this.file_components = Path(buildPath(cast(string) output_dir, 228 cast(string) file_prefix ~ "components" ~ fileExt)); 229 this.file_style_output = Path(buildPath(cast(string) output_dir, 230 cast(string) file_prefix ~ "style" ~ inclExt)); 231 this.file_style = Path(relativePath(cast(string) file_prefix ~ "style" ~ inclExt, 232 cast(string) output_dir)); 233 } 234 235 // -- Controller -- 236 237 bool doFile(in string filename, in string info) { 238 return fileFilter.match(filename, (string s, string type) { 239 logger.tracef("matcher --file-%s removed %s. Skipping", s, type); 240 }); 241 } 242 243 Flag!"genStyleInclFile" genStyleInclFile() { 244 import std.file : exists; 245 246 return cast(Flag!"genStyleInclFile")(do_style_incl && !exists(cast(string) file_style)); 247 } 248 249 Path doComponentNameStrip(Path fname) { 250 import std.path : dirName; 251 import cpptooling.testdouble.header_filter : stripFile; 252 253 if (do_comp_by_file) { 254 return Path(stripFile(cast(string) fname, comp_strip)); 255 } else { 256 return Path(stripFile((cast(string) fname).dirName, comp_strip)); 257 } 258 } 259 260 // -- Parameters -- 261 262 Path getOutputDirectory() const { 263 return output_dir; 264 } 265 266 Parameters.Files getFiles() const { 267 return Parameters.Files(file_classes, file_components, file_style, file_style_output); 268 } 269 270 FilePrefix getFilePrefix() const { 271 return file_prefix; 272 } 273 274 Flag!"genClassMethod" genClassMethod() const { 275 return gen_class_method; 276 } 277 278 Flag!"genClassParamDependency" genClassParamDependency() const { 279 return gen_class_param_dep; 280 } 281 282 Flag!"genClassInheritDependency" genClassInheritDependency() const { 283 return gen_class_inherit_dep; 284 } 285 286 Flag!"genClassMemberDependency" genClassMemberDependency() const { 287 return gen_class_member_dep; 288 } 289 290 Flag!"doStyleIncl" doStyleIncl() const { 291 return do_style_incl; 292 } 293 294 Flag!"doGenDot" doGenDot() const { 295 return do_gen_dot; 296 } 297 298 // -- Products -- 299 300 void putFile(Path fname, PlantumlRootModule root) { 301 fileData ~= FileData(fname, root.render()); 302 } 303 304 void putFile(Path fname, PlantumlModule pm) { 305 fileData ~= FileData(fname, pm.render()); 306 } 307 } 308 309 struct Lookup { 310 import cpptooling.data.symbol : Container, USRType; 311 import cpptooling.data : Location, LocationTag, TypeKind; 312 313 private Container* container; 314 315 auto kind(USRType usr) @safe { 316 return container.find!TypeKind(usr); 317 } 318 319 auto location(USRType usr) @safe { 320 return container.find!LocationTag(usr); 321 } 322 } 323 324 ExitStatusType genUml(PlantUMLFrontend variant, string[] in_cflags, 325 CompileCommandDB compile_db, Path[] inFiles, Flag!"skipFileError" skipFileError) { 326 import std.algorithm : map, joiner; 327 import std.array : array; 328 import std.conv : text; 329 import std.path : buildNormalizedPath, asAbsolutePath; 330 import std.typecons : Yes; 331 332 import cpptooling.data : CppRoot; 333 import cpptooling.data.symbol : Container; 334 import libclang_ast.context : ClangContext; 335 336 import dextool.clang : reduceMissingFiles; 337 import dextool.io : writeFileData; 338 import dextool.plugin.backend.plantuml : Generator, UMLVisitor, 339 UMLClassDiagram, UMLComponentDiagram, TransformToDiagram; 340 import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile; 341 342 Container container; 343 auto generator = Generator(variant, variant, variant); 344 345 // note how the transform is connected with destinations via the generator 346 // uml diagrams 347 auto transform = new TransformToDiagram!(Controller, Parameters, Lookup)(variant, 348 variant, Lookup(&container), generator.umlComponent, generator.umlClass); 349 350 auto visitor = new UMLVisitor!(Controller, typeof(transform))(variant, transform, container); 351 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 352 353 auto compDbRange() { 354 if (compile_db.empty) { 355 return fileRange(inFiles, Compiler("/usr/bin/c++")); 356 } 357 return compile_db.fileRange; 358 } 359 360 auto fixedDb = compDbRange.parse(defaultCompilerFilter).addCompiler(Compiler("/usr/bin/c++")) 361 .addSystemIncludes.prependFlags(prependDefaultFlags(in_cflags, PreferLang.none)).array; 362 363 auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array) 364 .reduceMissingFiles(fixedDb); 365 366 if (!compile_db.empty && !limitRange.isMissingFilesEmpty) { 367 foreach (a; limitRange.missingFiles) { 368 logger.error("Unable to find any compiler flags for .", a); 369 } 370 return ExitStatusType.Errors; 371 } 372 373 AbsolutePath[] unable_to_parse; 374 375 foreach (entry; limitRange.range) { 376 auto analyze_status = analyzeFile(entry.cmd.absoluteFile, 377 entry.flags.completeFlags, visitor, ctx); 378 379 // compile error, let user decide how to proceed. 380 if (analyze_status == ExitStatusType.Errors && skipFileError) { 381 logger.errorf("Continue analyze..."); 382 unable_to_parse ~= entry.cmd.absoluteFile; 383 } else if (analyze_status == ExitStatusType.Errors) { 384 return ExitStatusType.Errors; 385 } 386 } 387 388 if (unable_to_parse.length > 0) { 389 // TODO be aware that no test exist for this logic 390 import std.ascii : newline; 391 import std.range : roundRobin, repeat; 392 393 logger.errorf("Compile errors in the following files:\n%s\n", 394 unable_to_parse.map!(a => (cast(string) a)) 395 .roundRobin(newline.repeat(unable_to_parse.length)).joiner().text); 396 } 397 398 transform.finalize(); 399 generator.process(); 400 401 debug { 402 logger.trace(container.toString); 403 logger.trace(generator.umlComponent.toString); 404 logger.trace(generator.umlClass.toString); 405 } 406 407 return writeFileData(variant.fileData); 408 }