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, FileName(null)); 152 } 153 154 static auto make(FileName input_file) { 155 return FileProcess(Directive.Single, input_file); 156 } 157 158 Directive directive; 159 FileName 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 dextool.type : FileName, DirName, FilePrefix; 171 import dextool.utility; 172 173 import dsrcgen.plantuml; 174 175 static struct FileData { 176 import dextool.type : WriteStrategy; 177 178 FileName filename; 179 string data; 180 WriteStrategy strategy; 181 } 182 183 static const fileExt = ".pu"; 184 static const inclExt = ".iuml"; 185 186 // TODO ugly hack to remove immutable. Fix it appropriately 187 FileNames input_files; 188 immutable DirName output_dir; 189 immutable FileName file_classes; 190 immutable FileName file_components; 191 immutable FileName file_style; 192 immutable FileName file_style_output; 193 194 immutable FilePrefix file_prefix; 195 196 immutable Flag!"genClassMethod" gen_class_method; 197 immutable Flag!"genClassParamDependency" gen_class_param_dep; 198 immutable Flag!"genClassInheritDependency" gen_class_inherit_dep; 199 immutable Flag!"genClassMemberDependency" gen_class_member_dep; 200 immutable Flag!"doStyleIncl" do_style_incl; 201 immutable Flag!"doGenDot" do_gen_dot; 202 immutable Flag!"doComponentByFile" do_comp_by_file; 203 204 Regex!char[] exclude; 205 Regex!char[] restrict; 206 Regex!char comp_strip; 207 208 /// Data produced by the generator intended to be written to specified file. 209 FileData[] fileData; 210 211 static auto makeVariant(ref RawConfiguration parsed) { 212 import std.algorithm : map; 213 import std.array : array; 214 215 Regex!char[] exclude = parsed.fileExclude.map!(a => regex(a)).array(); 216 Regex!char[] restrict = parsed.fileRestrict.map!(a => regex(a)).array(); 217 Regex!char comp_strip; 218 219 if (parsed.componentStrip.length != 0) { 220 comp_strip = regex(parsed.componentStrip); 221 } 222 223 auto gen_class_method = cast(Flag!"genClassMethod") parsed.classMethod; 224 auto gen_class_param_dep = cast(Flag!"genClassParamDependency") parsed.classParamDep; 225 auto gen_class_inherit_dep = cast(Flag!"genClassInheritDependency") parsed.classInheritDep; 226 auto gen_class_member_dep = cast(Flag!"genClassMemberDependency") parsed.classMemberDep; 227 228 auto gen_style_incl = cast(Flag!"doStyleIncl") parsed.generateStyleInclude; 229 auto gen_dot = cast(Flag!"doGenDot") parsed.generateDot; 230 auto do_comp_by_file = cast(Flag!"doComponentByFile") parsed.componentByFile; 231 232 auto variant = new PlantUMLFrontend(FilePrefix(parsed.filePrefix), 233 DirName(parsed.out_), gen_style_incl, gen_dot, 234 gen_class_method, gen_class_param_dep, gen_class_inherit_dep, 235 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, DirName 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 = FileName(buildPath(cast(string) output_dir, 262 cast(string) file_prefix ~ "classes" ~ fileExt)); 263 this.file_components = FileName(buildPath(cast(string) output_dir, 264 cast(string) file_prefix ~ "components" ~ fileExt)); 265 this.file_style_output = FileName(buildPath(cast(string) output_dir, 266 cast(string) file_prefix ~ "style" ~ inclExt)); 267 this.file_style = FileName(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 FileName doComponentNameStrip(FileName fname) { 303 import std.path : dirName; 304 import cpptooling.testdouble.header_filter : stripFile; 305 306 if (do_comp_by_file) { 307 return FileName(stripFile(cast(string) fname, comp_strip)); 308 } else { 309 return FileName(stripFile((cast(string) fname).dirName, comp_strip)); 310 } 311 } 312 313 // -- Parameters -- 314 315 DirName 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(FileName fname, PlantumlRootModule root) { 354 fileData ~= FileData(fname, root.render()); 355 } 356 357 void putFile(FileName 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 CompileCommand.AbsoluteFileName[] 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(FileName(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 }