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; 15 16 import logger = std.experimental.logger; 17 18 import dextool.compilation_db; 19 import dextool.type; 20 21 import dextool.plugin.types; 22 import dextool.plugin.backend.plantuml : Controller, Parameters, Products; 23 import cpptooling.data : CppRoot, CppNamespace, CppClass; 24 25 struct RawConfiguration { 26 string[] cflags; 27 string[] compileDb; 28 string[] fileExclude; 29 string[] fileRestrict; 30 string[] inFiles; 31 string componentStrip; 32 string filePrefix = "view_"; 33 string out_; 34 bool classInheritDep; 35 bool classMemberDep; 36 bool classMethod; 37 bool classParamDep; 38 bool componentByFile; 39 bool generateDot; 40 bool generateStyleInclude; 41 bool help; 42 bool shortPluginHelp; 43 bool skipFileError; 44 45 string[] originalFlags; 46 47 void parse(string[] args) { 48 import std.getopt; 49 50 originalFlags = args.dup; 51 52 // dfmt off 53 try { 54 getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help, 55 "class-method", &classMethod, 56 "class-paramdep", &classParamDep, 57 "class-inheritdep", &classInheritDep, 58 "class-memberdep", &classMemberDep, 59 "compile-db", &compileDb, 60 "comp-by-file", &componentByFile, 61 "comp-strip", &componentStrip, 62 "file-exclude", &fileExclude, 63 "file-prefix", &filePrefix, 64 "file-restrict", &fileRestrict, 65 "gen-dot", &generateDot, 66 "gen-style-incl", &generateStyleInclude, 67 "in", &inFiles, 68 "out", &out_, 69 "short-plugin-help", &shortPluginHelp, 70 "skip-file-error", &skipFileError, 71 ); 72 } 73 catch (std.getopt.GetOptException ex) { 74 logger.error(ex.msg); 75 help = true; 76 } 77 // dfmt on 78 79 import std.algorithm : find; 80 import std.array : array; 81 import std.range : drop; 82 83 // at this point args contain "what is left". What is interesting then is those after "--". 84 cflags = args.find("--").drop(1).array(); 85 } 86 87 void printHelp() { 88 import std.stdio : writefln; 89 90 writefln("%s\n\n%s\n%s", plantuml_opt.usage, plantuml_opt.optional, plantuml_opt.others); 91 } 92 93 void dump() { 94 logger.trace(this); 95 } 96 } 97 98 // dfmt off 99 static auto plantuml_opt = CliOptionParts( 100 "usage: 101 dextool uml [options] [--compile-db=...] [--file-exclude=...] [--in=...] [--] [CFLAGS...] 102 dextool uml [options] [--compile-db=...] [--file-restrict=...] [--in=...] [--] [CFLAGS...]", 103 // ------------- 104 " --out=dir directory for generated files [default: ./] 105 --file-prefix=p Prefix used when generating test artifacts [default: view_] 106 --class-method Include methods in the generated class diagram 107 --class-paramdep Class method parameters as directed association in diagram 108 --class-inheritdep Class inheritance in diagram 109 --class-memberdep Class member as composition/aggregation in diagram 110 --comp-by-file Components by file instead of directory 111 --comp-strip=r Regex used to strip path used to derive component name 112 --gen-style-incl Generate a style file and include in all diagrams 113 --gen-dot Generate a dot graph block in the plantuml output 114 --skip-file-error Skip files that result in compile errors (only when using compile-db and processing all files)", 115 // ------------- 116 "others: 117 --in= Input files to parse 118 --compile-db=j Retrieve compilation parameters from the file 119 --file-exclude= Exclude files from generation matching the regex 120 --file-restrict= Restrict the scope of the test double to those files 121 matching the regex 122 123 REGEX 124 The regex syntax is found at http://dlang.org/phobos/std_regex.html 125 126 Information about --file-exclude. 127 The regex must fully match the filename the AST node is located in. 128 If it matches all data from the file is excluded from the generated code. 129 130 Information about --file-restrict. 131 The regex must fully match the filename the AST node is located in. 132 Only symbols from files matching the restrict affect the generated test double. 133 " 134 ); 135 // dfmt on 136 137 /** Contains the file processing directives after parsing user arguments. 138 * 139 * If no --in argument then it is assumed that all files in the CompileDB 140 * shall be processed. 141 * 142 * Indicated by the directive All. 143 */ 144 struct FileProcess { 145 enum Directive { 146 Single, 147 All 148 } 149 150 static auto make() { 151 return FileProcess(Directive.All, Path(null)); 152 } 153 154 static auto make(Path input_file) { 155 return FileProcess(Directive.Single, input_file); 156 } 157 158 Directive directive; 159 Path inputFile; 160 } 161 162 /** Frontend for PlantUML generator. 163 * 164 * TODO implement --in=... for multi-file handling 165 */ 166 class PlantUMLFrontend : Controller, Parameters, Products { 167 import std.string : toLower; 168 import std.regex : regex, Regex; 169 import std.typecons : Flag, Yes, No; 170 import cpptooling.type : FilePrefix; 171 import dextool.type : Path; 172 import dextool.utility; 173 174 import dsrcgen.plantuml; 175 176 static struct FileData { 177 import dextool.io : WriteStrategy; 178 179 Path filename; 180 string data; 181 WriteStrategy strategy; 182 } 183 184 static const fileExt = ".pu"; 185 static const inclExt = ".iuml"; 186 187 // TODO ugly hack to remove immutable. Fix it appropriately 188 Path[] input_files; 189 immutable Path output_dir; 190 immutable Path file_classes; 191 immutable Path file_components; 192 immutable Path file_style; 193 immutable Path file_style_output; 194 195 immutable FilePrefix file_prefix; 196 197 immutable Flag!"genClassMethod" gen_class_method; 198 immutable Flag!"genClassParamDependency" gen_class_param_dep; 199 immutable Flag!"genClassInheritDependency" gen_class_inherit_dep; 200 immutable Flag!"genClassMemberDependency" gen_class_member_dep; 201 immutable Flag!"doStyleIncl" do_style_incl; 202 immutable Flag!"doGenDot" do_gen_dot; 203 immutable Flag!"doComponentByFile" do_comp_by_file; 204 205 Regex!char[] exclude; 206 Regex!char[] restrict; 207 Regex!char comp_strip; 208 209 /// Data produced by the generator intended to be written to specified file. 210 FileData[] fileData; 211 212 static auto makeVariant(ref RawConfiguration parsed) { 213 import std.algorithm : map; 214 import std.array : array; 215 216 Regex!char[] exclude = parsed.fileExclude.map!(a => regex(a)).array(); 217 Regex!char[] restrict = parsed.fileRestrict.map!(a => regex(a)).array(); 218 Regex!char comp_strip; 219 220 if (parsed.componentStrip.length != 0) { 221 comp_strip = regex(parsed.componentStrip); 222 } 223 224 auto gen_class_method = cast(Flag!"genClassMethod") parsed.classMethod; 225 auto gen_class_param_dep = cast(Flag!"genClassParamDependency") parsed.classParamDep; 226 auto gen_class_inherit_dep = cast(Flag!"genClassInheritDependency") parsed.classInheritDep; 227 auto gen_class_member_dep = cast(Flag!"genClassMemberDependency") parsed.classMemberDep; 228 229 auto gen_style_incl = cast(Flag!"doStyleIncl") parsed.generateStyleInclude; 230 auto gen_dot = cast(Flag!"doGenDot") parsed.generateDot; 231 auto do_comp_by_file = cast(Flag!"doComponentByFile") parsed.componentByFile; 232 233 auto variant = new PlantUMLFrontend(FilePrefix(parsed.filePrefix), 234 Path(parsed.out_), gen_style_incl, gen_dot, gen_class_method, 235 gen_class_param_dep, gen_class_inherit_dep, gen_class_member_dep, do_comp_by_file); 236 237 variant.exclude = exclude; 238 variant.restrict = restrict; 239 variant.comp_strip = comp_strip; 240 241 return variant; 242 } 243 244 this(FilePrefix file_prefix, Path output_dir, Flag!"doStyleIncl" style_incl, 245 Flag!"doGenDot" gen_dot, Flag!"genClassMethod" class_method, 246 Flag!"genClassParamDependency" class_param_dep, Flag!"genClassInheritDependency" class_inherit_dep, 247 Flag!"genClassMemberDependency" class_member_dep, 248 Flag!"doComponentByFile" do_comp_by_file) { 249 this.file_prefix = file_prefix; 250 this.output_dir = output_dir; 251 this.gen_class_method = class_method; 252 this.gen_class_param_dep = class_param_dep; 253 this.gen_class_inherit_dep = class_inherit_dep; 254 this.gen_class_member_dep = class_member_dep; 255 this.do_comp_by_file = do_comp_by_file; 256 this.do_gen_dot = gen_dot; 257 this.do_style_incl = style_incl; 258 259 import std.path : baseName, buildPath, relativePath, stripExtension; 260 261 this.file_classes = Path(buildPath(cast(string) output_dir, 262 cast(string) file_prefix ~ "classes" ~ fileExt)); 263 this.file_components = Path(buildPath(cast(string) output_dir, 264 cast(string) file_prefix ~ "components" ~ fileExt)); 265 this.file_style_output = Path(buildPath(cast(string) output_dir, 266 cast(string) file_prefix ~ "style" ~ inclExt)); 267 this.file_style = Path(relativePath(cast(string) file_prefix ~ "style" ~ inclExt, 268 cast(string) output_dir)); 269 } 270 271 // -- Controller -- 272 273 bool doFile(in string filename, in string info) { 274 import dextool.plugin.regex_matchers : matchAny; 275 276 bool restrict_pass = true; 277 bool exclude_pass = true; 278 279 if (restrict.length > 0) { 280 restrict_pass = matchAny(filename, restrict); 281 debug { 282 logger.tracef(!restrict_pass, "--file-restrict skipping %s", info); 283 } 284 } 285 286 if (exclude.length > 0) { 287 exclude_pass = !matchAny(filename, exclude); 288 debug { 289 logger.tracef(!exclude_pass, "--file-exclude skipping %s", info); 290 } 291 } 292 293 return restrict_pass && exclude_pass; 294 } 295 296 Flag!"genStyleInclFile" genStyleInclFile() { 297 import std.file : exists; 298 299 return cast(Flag!"genStyleInclFile")(do_style_incl && !exists(cast(string) file_style)); 300 } 301 302 Path doComponentNameStrip(Path fname) { 303 import std.path : dirName; 304 import cpptooling.testdouble.header_filter : stripFile; 305 306 if (do_comp_by_file) { 307 return Path(stripFile(cast(string) fname, comp_strip)); 308 } else { 309 return Path(stripFile((cast(string) fname).dirName, comp_strip)); 310 } 311 } 312 313 // -- Parameters -- 314 315 Path getOutputDirectory() const { 316 return output_dir; 317 } 318 319 Parameters.Files getFiles() const { 320 return Parameters.Files(file_classes, file_components, file_style, file_style_output); 321 } 322 323 FilePrefix getFilePrefix() const { 324 return file_prefix; 325 } 326 327 Flag!"genClassMethod" genClassMethod() const { 328 return gen_class_method; 329 } 330 331 Flag!"genClassParamDependency" genClassParamDependency() const { 332 return gen_class_param_dep; 333 } 334 335 Flag!"genClassInheritDependency" genClassInheritDependency() const { 336 return gen_class_inherit_dep; 337 } 338 339 Flag!"genClassMemberDependency" genClassMemberDependency() const { 340 return gen_class_member_dep; 341 } 342 343 Flag!"doStyleIncl" doStyleIncl() const { 344 return do_style_incl; 345 } 346 347 Flag!"doGenDot" doGenDot() const { 348 return do_gen_dot; 349 } 350 351 // -- Products -- 352 353 void putFile(Path fname, PlantumlRootModule root) { 354 fileData ~= FileData(fname, root.render()); 355 } 356 357 void putFile(Path fname, PlantumlModule pm) { 358 fileData ~= FileData(fname, pm.render()); 359 } 360 } 361 362 struct Lookup { 363 import cpptooling.data.symbol : Container, USRType; 364 import cpptooling.data : Location, LocationTag, TypeKind; 365 366 private Container* container; 367 368 auto kind(USRType usr) @safe { 369 return container.find!TypeKind(usr); 370 } 371 372 auto location(USRType usr) @safe { 373 return container.find!LocationTag(usr); 374 } 375 } 376 377 ExitStatusType genUml(PlantUMLFrontend variant, string[] in_cflags, 378 CompileCommandDB compile_db, FileProcess file_process, Flag!"skipFileError" skipFileError) { 379 import std.algorithm : map, joiner; 380 import std.conv : text; 381 import std.path : buildNormalizedPath, asAbsolutePath; 382 import std.typecons : Yes; 383 384 import cpptooling.data : CppRoot; 385 import cpptooling.data.symbol : Container; 386 387 import cpptooling.analyzer.clang.context : ClangContext; 388 import dextool.io : writeFileData; 389 import dextool.plugin.backend.plantuml : Generator, UMLVisitor, 390 UMLClassDiagram, UMLComponentDiagram, TransformToDiagram; 391 import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile; 392 393 Container container; 394 auto generator = Generator(variant, variant, variant); 395 396 // note how the transform is connected with destinations via the generator 397 // uml diagrams 398 auto transform = new TransformToDiagram!(Controller, Parameters, Lookup)(variant, 399 variant, Lookup(&container), generator.umlComponent, generator.umlClass); 400 401 auto visitor = new UMLVisitor!(Controller, typeof(transform))(variant, transform, container); 402 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 403 404 final switch (file_process.directive) { 405 case FileProcess.Directive.All: 406 const auto cflags = prependDefaultFlags(in_cflags, PreferLang.none); 407 AbsolutePath[] unable_to_parse; 408 409 const auto total_files = compile_db.length; 410 411 foreach (idx, entry; compile_db) { 412 logger.infof("File %d/%d ", idx + 1, total_files); 413 auto entry_cflags = cflags ~ parseFlag(entry, defaultCompilerFilter); 414 415 auto analyze_status = analyzeFile(entry.absoluteFile, entry_cflags, visitor, ctx); 416 417 // compile error, let user decide how to proceed. 418 if (analyze_status == ExitStatusType.Errors && skipFileError) { 419 logger.errorf("Continue analyze..."); 420 unable_to_parse ~= entry.absoluteFile; 421 } else if (analyze_status == ExitStatusType.Errors) { 422 return ExitStatusType.Errors; 423 } 424 } 425 426 if (unable_to_parse.length > 0) { 427 // TODO be aware that no test exist for this logic 428 import std.ascii : newline; 429 import std.range : roundRobin, repeat; 430 431 logger.errorf("Compile errors in the following files:\n%s\n", 432 unable_to_parse.map!(a => (cast(string) a)) 433 .roundRobin(newline.repeat(unable_to_parse.length)).joiner().text); 434 } 435 break; 436 437 case FileProcess.Directive.Single: 438 const auto user_cflags = prependDefaultFlags(in_cflags, PreferLang.none); 439 440 string[] use_cflags; 441 AbsolutePath abs_in_file; 442 string input_file = cast(string) file_process.inputFile; 443 444 logger.trace("Input file: ", input_file); 445 446 if (compile_db.length > 0) { 447 auto db_search_result = compile_db.appendOrError(user_cflags, input_file); 448 if (db_search_result.isNull) { 449 return ExitStatusType.Errors; 450 } 451 use_cflags = db_search_result.get.cflags; 452 abs_in_file = db_search_result.get.absoluteFile; 453 } else { 454 use_cflags = user_cflags.dup; 455 abs_in_file = AbsolutePath(Path(input_file)); 456 } 457 458 if (analyzeFile(abs_in_file, use_cflags, visitor, ctx) == ExitStatusType.Errors) { 459 return ExitStatusType.Errors; 460 } 461 break; 462 } 463 464 transform.finalize(); 465 generator.process(); 466 467 debug { 468 logger.trace(container.toString); 469 logger.trace(generator.umlComponent.toString); 470 logger.trace(generator.umlClass.toString); 471 } 472 473 return writeFileData(variant.fileData); 474 }