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 }