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 }