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 }