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 /** Frontend for PlantUML generator. 138 * 139 * TODO implement --in=... for multi-file handling 140 */ 141 class PlantUMLFrontend : Controller, Parameters, Products { 142 import std.string : toLower; 143 import std.regex : regex, Regex; 144 import std.typecons : Flag, Yes, No; 145 import cpptooling.type : FilePrefix; 146 import dextool.type : Path; 147 import dextool.utility; 148 149 import dsrcgen.plantuml; 150 151 static struct FileData { 152 import dextool.io : WriteStrategy; 153 154 Path filename; 155 string data; 156 WriteStrategy strategy; 157 } 158 159 static const fileExt = ".pu"; 160 static const inclExt = ".iuml"; 161 162 // TODO ugly hack to remove immutable. Fix it appropriately 163 Path[] input_files; 164 immutable Path output_dir; 165 immutable Path file_classes; 166 immutable Path file_components; 167 immutable Path file_style; 168 immutable Path file_style_output; 169 170 immutable FilePrefix file_prefix; 171 172 immutable Flag!"genClassMethod" gen_class_method; 173 immutable Flag!"genClassParamDependency" gen_class_param_dep; 174 immutable Flag!"genClassInheritDependency" gen_class_inherit_dep; 175 immutable Flag!"genClassMemberDependency" gen_class_member_dep; 176 immutable Flag!"doStyleIncl" do_style_incl; 177 immutable Flag!"doGenDot" do_gen_dot; 178 immutable Flag!"doComponentByFile" do_comp_by_file; 179 180 Regex!char[] exclude; 181 Regex!char[] restrict; 182 Regex!char comp_strip; 183 184 /// Data produced by the generator intended to be written to specified file. 185 FileData[] fileData; 186 187 static auto makeVariant(ref RawConfiguration parsed) { 188 import std.algorithm : map; 189 import std.array : array; 190 191 Regex!char[] exclude = parsed.fileExclude.map!(a => regex(a)).array(); 192 Regex!char[] restrict = parsed.fileRestrict.map!(a => regex(a)).array(); 193 Regex!char comp_strip; 194 195 if (parsed.componentStrip.length != 0) { 196 comp_strip = regex(parsed.componentStrip); 197 } 198 199 auto gen_class_method = cast(Flag!"genClassMethod") parsed.classMethod; 200 auto gen_class_param_dep = cast(Flag!"genClassParamDependency") parsed.classParamDep; 201 auto gen_class_inherit_dep = cast(Flag!"genClassInheritDependency") parsed.classInheritDep; 202 auto gen_class_member_dep = cast(Flag!"genClassMemberDependency") parsed.classMemberDep; 203 204 auto gen_style_incl = cast(Flag!"doStyleIncl") parsed.generateStyleInclude; 205 auto gen_dot = cast(Flag!"doGenDot") parsed.generateDot; 206 auto do_comp_by_file = cast(Flag!"doComponentByFile") parsed.componentByFile; 207 208 auto variant = new PlantUMLFrontend(FilePrefix(parsed.filePrefix), 209 Path(parsed.out_), gen_style_incl, gen_dot, gen_class_method, 210 gen_class_param_dep, gen_class_inherit_dep, gen_class_member_dep, do_comp_by_file); 211 212 variant.exclude = exclude; 213 variant.restrict = restrict; 214 variant.comp_strip = comp_strip; 215 216 return variant; 217 } 218 219 this(FilePrefix file_prefix, Path output_dir, Flag!"doStyleIncl" style_incl, 220 Flag!"doGenDot" gen_dot, Flag!"genClassMethod" class_method, 221 Flag!"genClassParamDependency" class_param_dep, Flag!"genClassInheritDependency" class_inherit_dep, 222 Flag!"genClassMemberDependency" class_member_dep, 223 Flag!"doComponentByFile" do_comp_by_file) { 224 this.file_prefix = file_prefix; 225 this.output_dir = output_dir; 226 this.gen_class_method = class_method; 227 this.gen_class_param_dep = class_param_dep; 228 this.gen_class_inherit_dep = class_inherit_dep; 229 this.gen_class_member_dep = class_member_dep; 230 this.do_comp_by_file = do_comp_by_file; 231 this.do_gen_dot = gen_dot; 232 this.do_style_incl = style_incl; 233 234 import std.path : baseName, buildPath, relativePath, stripExtension; 235 236 this.file_classes = Path(buildPath(cast(string) output_dir, 237 cast(string) file_prefix ~ "classes" ~ fileExt)); 238 this.file_components = Path(buildPath(cast(string) output_dir, 239 cast(string) file_prefix ~ "components" ~ fileExt)); 240 this.file_style_output = Path(buildPath(cast(string) output_dir, 241 cast(string) file_prefix ~ "style" ~ inclExt)); 242 this.file_style = Path(relativePath(cast(string) file_prefix ~ "style" ~ inclExt, 243 cast(string) output_dir)); 244 } 245 246 // -- Controller -- 247 248 bool doFile(in string filename, in string info) { 249 import dextool.plugin.regex_matchers : matchAny; 250 251 bool restrict_pass = true; 252 bool exclude_pass = true; 253 254 if (restrict.length > 0) { 255 restrict_pass = matchAny(filename, restrict); 256 debug { 257 logger.tracef(!restrict_pass, "--file-restrict skipping %s", info); 258 } 259 } 260 261 if (exclude.length > 0) { 262 exclude_pass = !matchAny(filename, exclude); 263 debug { 264 logger.tracef(!exclude_pass, "--file-exclude skipping %s", info); 265 } 266 } 267 268 return restrict_pass && exclude_pass; 269 } 270 271 Flag!"genStyleInclFile" genStyleInclFile() { 272 import std.file : exists; 273 274 return cast(Flag!"genStyleInclFile")(do_style_incl && !exists(cast(string) file_style)); 275 } 276 277 Path doComponentNameStrip(Path fname) { 278 import std.path : dirName; 279 import cpptooling.testdouble.header_filter : stripFile; 280 281 if (do_comp_by_file) { 282 return Path(stripFile(cast(string) fname, comp_strip)); 283 } else { 284 return Path(stripFile((cast(string) fname).dirName, comp_strip)); 285 } 286 } 287 288 // -- Parameters -- 289 290 Path getOutputDirectory() const { 291 return output_dir; 292 } 293 294 Parameters.Files getFiles() const { 295 return Parameters.Files(file_classes, file_components, file_style, file_style_output); 296 } 297 298 FilePrefix getFilePrefix() const { 299 return file_prefix; 300 } 301 302 Flag!"genClassMethod" genClassMethod() const { 303 return gen_class_method; 304 } 305 306 Flag!"genClassParamDependency" genClassParamDependency() const { 307 return gen_class_param_dep; 308 } 309 310 Flag!"genClassInheritDependency" genClassInheritDependency() const { 311 return gen_class_inherit_dep; 312 } 313 314 Flag!"genClassMemberDependency" genClassMemberDependency() const { 315 return gen_class_member_dep; 316 } 317 318 Flag!"doStyleIncl" doStyleIncl() const { 319 return do_style_incl; 320 } 321 322 Flag!"doGenDot" doGenDot() const { 323 return do_gen_dot; 324 } 325 326 // -- Products -- 327 328 void putFile(Path fname, PlantumlRootModule root) { 329 fileData ~= FileData(fname, root.render()); 330 } 331 332 void putFile(Path fname, PlantumlModule pm) { 333 fileData ~= FileData(fname, pm.render()); 334 } 335 } 336 337 struct Lookup { 338 import cpptooling.data.symbol : Container, USRType; 339 import cpptooling.data : Location, LocationTag, TypeKind; 340 341 private Container* container; 342 343 auto kind(USRType usr) @safe { 344 return container.find!TypeKind(usr); 345 } 346 347 auto location(USRType usr) @safe { 348 return container.find!LocationTag(usr); 349 } 350 } 351 352 ExitStatusType genUml(PlantUMLFrontend variant, string[] in_cflags, 353 CompileCommandDB compile_db, Path[] inFiles, Flag!"skipFileError" skipFileError) { 354 import std.algorithm : map, joiner; 355 import std.array : array; 356 import std.conv : text; 357 import std.path : buildNormalizedPath, asAbsolutePath; 358 import std.typecons : Yes; 359 360 import cpptooling.data : CppRoot; 361 import cpptooling.data.symbol : Container; 362 import cpptooling.analyzer.clang.context : ClangContext; 363 364 import dextool.clang : reduceMissingFiles; 365 import dextool.io : writeFileData; 366 import dextool.plugin.backend.plantuml : Generator, UMLVisitor, 367 UMLClassDiagram, UMLComponentDiagram, TransformToDiagram; 368 import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile; 369 370 Container container; 371 auto generator = Generator(variant, variant, variant); 372 373 // note how the transform is connected with destinations via the generator 374 // uml diagrams 375 auto transform = new TransformToDiagram!(Controller, Parameters, Lookup)(variant, 376 variant, Lookup(&container), generator.umlComponent, generator.umlClass); 377 378 auto visitor = new UMLVisitor!(Controller, typeof(transform))(variant, transform, container); 379 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly); 380 381 auto compDbRange() { 382 if (compile_db.empty) { 383 return fileRange(inFiles, Compiler("/usr/bin/c++")); 384 } 385 return compile_db.fileRange; 386 } 387 388 auto fixedDb = compDbRange.parse(defaultCompilerFilter).addCompiler(Compiler("/usr/bin/c++")) 389 .addSystemIncludes.prependFlags(prependDefaultFlags(in_cflags, PreferLang.none)).array; 390 391 auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array) 392 .reduceMissingFiles(fixedDb); 393 394 if (!compile_db.empty && !limitRange.isMissingFilesEmpty) { 395 foreach (a; limitRange.missingFiles) { 396 logger.error("Unable to find any compiler flags for .", a); 397 } 398 return ExitStatusType.Errors; 399 } 400 401 AbsolutePath[] unable_to_parse; 402 403 foreach (entry; limitRange.range) { 404 auto analyze_status = analyzeFile(entry.cmd.absoluteFile, 405 entry.flags.completeFlags, visitor, ctx); 406 407 // compile error, let user decide how to proceed. 408 if (analyze_status == ExitStatusType.Errors && skipFileError) { 409 logger.errorf("Continue analyze..."); 410 unable_to_parse ~= entry.cmd.absoluteFile; 411 } else if (analyze_status == ExitStatusType.Errors) { 412 return ExitStatusType.Errors; 413 } 414 } 415 416 if (unable_to_parse.length > 0) { 417 // TODO be aware that no test exist for this logic 418 import std.ascii : newline; 419 import std.range : roundRobin, repeat; 420 421 logger.errorf("Compile errors in the following files:\n%s\n", 422 unable_to_parse.map!(a => (cast(string) a)) 423 .roundRobin(newline.repeat(unable_to_parse.length)).joiner().text); 424 } 425 426 transform.finalize(); 427 generator.process(); 428 429 debug { 430 logger.trace(container.toString); 431 logger.trace(generator.umlComponent.toString); 432 logger.trace(generator.umlClass.toString); 433 } 434 435 return writeFileData(variant.fileData); 436 }