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, Tuple;
15 import logger = std.experimental.logger;
16 
17 import dextool.compilation_db;
18 import dextool.type;
19 
20 import dextool.plugin.backend.plantuml : Controller, Parameters, Products;
21 import cpptooling.data : CppRoot, CppNamespace, CppClass;
22 
23 struct RawConfiguration {
24     string[] cflags;
25     string[] compileDb;
26     string[] fileExclude;
27     string[] fileInclude;
28     string[] inFiles;
29     string componentStrip;
30     string filePrefix = "view_";
31     string out_;
32     bool classInheritDep;
33     bool classMemberDep;
34     bool classMethod;
35     bool classParamDep;
36     bool componentByFile;
37     bool generateDot;
38     bool generateStyleInclude;
39     bool help;
40     bool shortPluginHelp;
41     bool skipFileError;
42 
43     string[] originalFlags;
44 
45     void parse(string[] args) {
46         import std.getopt;
47 
48         originalFlags = args.dup;
49 
50         // dfmt off
51         try {
52             getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help,
53                    "class-method", &classMethod,
54                    "class-paramdep", &classParamDep,
55                    "class-inheritdep", &classInheritDep,
56                    "class-memberdep", &classMemberDep,
57                    "compile-db", &compileDb,
58                    "comp-by-file", &componentByFile,
59                    "comp-strip", &componentStrip,
60                    "file-exclude", &fileExclude,
61                    "file-prefix", &filePrefix,
62                    "file-include", &fileInclude,
63                    "gen-dot", &generateDot,
64                    "gen-style-incl", &generateStyleInclude,
65                    "in", &inFiles,
66                    "out", &out_,
67                    "short-plugin-help", &shortPluginHelp,
68                    "skip-file-error", &skipFileError,
69                    );
70         }
71         catch (std.getopt.GetOptException ex) {
72             logger.error(ex.msg);
73             help = true;
74         }
75         // dfmt on
76 
77         import std.algorithm : find;
78         import std.array : array;
79         import std.range : drop;
80 
81         // at this point args contain "what is left". What is interesting then is those after "--".
82         cflags = args.find("--").drop(1).array();
83     }
84 
85     void printHelp() {
86         import std.stdio : writefln;
87 
88         writefln("%s\n\n%s\n%s", plantuml_opt.usage, plantuml_opt.optional, plantuml_opt.others);
89     }
90 
91     void dump() {
92         logger.trace(this);
93     }
94 }
95 
96 // dfmt off
97 static auto plantuml_opt = Tuple!(string, "usage", string, "optional", string, "others")(
98     "usage:
99  dextool uml [options] [--compile-db=...] [--file-exclude=...] [--in=...] [--] [CFLAGS...]
100  dextool uml [options] [--compile-db=...] [--file-include=...] [--in=...] [--] [CFLAGS...]",
101     // -------------
102     " --out=dir           directory for generated files [default: ./]
103  --file-prefix=p     Prefix used when generating test artifacts [default: view_]
104  --class-method      Include methods in the generated class diagram
105  --class-paramdep    Class method parameters as directed association in diagram
106  --class-inheritdep  Class inheritance in diagram
107  --class-memberdep   Class member as composition/aggregation in diagram
108  --comp-by-file      Components by file instead of directory
109  --comp-strip=r      Regex used to strip path used to derive component name
110  --gen-style-incl    Generate a style file and include in all diagrams
111  --gen-dot           Generate a dot graph block in the plantuml output
112  --skip-file-error   Skip files that result in compile errors (only when using compile-db and processing all files)",
113     // -------------
114 "others:
115  --in=               Input files to parse
116  --compile-db=j      Retrieve compilation parameters from the file
117  --file-exclude=     Exclude files from generation matching the regex
118  --file-include=     Include the scope of the test double to those files
119                      matching the regex
120 
121 REGEX
122 The regex syntax is found at http://dlang.org/phobos/std_regex.html
123 
124 Information about --file-exclude.
125   The regex must fully match the filename the AST node is located in.
126   If it matches all data from the file is excluded from the generated code.
127 
128 Information about --file-include.
129   The regex must fully match the filename the AST node is located in.
130   Only symbols from files matching the include affect the generated test double.
131 "
132 );
133 // dfmt on
134 
135 /** Frontend for PlantUML generator.
136  *
137  * TODO implement --in=... for multi-file handling
138  */
139 class PlantUMLFrontend : Controller, Parameters, Products {
140     import std.string : toLower;
141     import std.regex : regex, Regex;
142     import std.typecons : Flag, Yes, No;
143     import my.filter : ReFilter;
144     import cpptooling.type : FilePrefix;
145     import dextool.type : Path;
146 
147     import dsrcgen.plantuml;
148 
149     static struct FileData {
150         import dextool.io : WriteStrategy;
151 
152         Path filename;
153         string data;
154         WriteStrategy strategy;
155     }
156 
157     static const fileExt = ".pu";
158     static const inclExt = ".iuml";
159 
160     // TODO ugly hack to remove immutable. Fix it appropriately
161     Path[] input_files;
162     immutable Path output_dir;
163     immutable Path file_classes;
164     immutable Path file_components;
165     immutable Path file_style;
166     immutable Path file_style_output;
167 
168     immutable FilePrefix file_prefix;
169 
170     immutable Flag!"genClassMethod" gen_class_method;
171     immutable Flag!"genClassParamDependency" gen_class_param_dep;
172     immutable Flag!"genClassInheritDependency" gen_class_inherit_dep;
173     immutable Flag!"genClassMemberDependency" gen_class_member_dep;
174     immutable Flag!"doStyleIncl" do_style_incl;
175     immutable Flag!"doGenDot" do_gen_dot;
176     immutable Flag!"doComponentByFile" do_comp_by_file;
177 
178     ReFilter fileFilter;
179     Regex!char comp_strip;
180 
181     /// Data produced by the generator intended to be written to specified file.
182     FileData[] fileData;
183 
184     static auto makeVariant(ref RawConfiguration parsed) {
185         auto gen_class_method = cast(Flag!"genClassMethod") parsed.classMethod;
186         auto gen_class_param_dep = cast(Flag!"genClassParamDependency") parsed.classParamDep;
187         auto gen_class_inherit_dep = cast(Flag!"genClassInheritDependency") parsed.classInheritDep;
188         auto gen_class_member_dep = cast(Flag!"genClassMemberDependency") parsed.classMemberDep;
189 
190         auto gen_style_incl = cast(Flag!"doStyleIncl") parsed.generateStyleInclude;
191         auto gen_dot = cast(Flag!"doGenDot") parsed.generateDot;
192         auto do_comp_by_file = cast(Flag!"doComponentByFile") parsed.componentByFile;
193 
194         auto variant = new PlantUMLFrontend(FilePrefix(parsed.filePrefix),
195                 Path(parsed.out_), gen_style_incl, gen_dot, gen_class_method,
196                 gen_class_param_dep, gen_class_inherit_dep, gen_class_member_dep, do_comp_by_file);
197 
198         variant.fileFilter = ReFilter(parsed.fileInclude, parsed.fileExclude);
199         variant.comp_strip = () {
200             if (parsed.componentStrip.length != 0)
201                 return regex(parsed.componentStrip);
202             return Regex!char.init;
203         }();
204 
205         return variant;
206     }
207 
208     this(FilePrefix file_prefix, Path output_dir, Flag!"doStyleIncl" style_incl,
209             Flag!"doGenDot" gen_dot, Flag!"genClassMethod" class_method,
210             Flag!"genClassParamDependency" class_param_dep, Flag!"genClassInheritDependency" class_inherit_dep,
211             Flag!"genClassMemberDependency" class_member_dep,
212             Flag!"doComponentByFile" do_comp_by_file) {
213         this.file_prefix = file_prefix;
214         this.output_dir = output_dir;
215         this.gen_class_method = class_method;
216         this.gen_class_param_dep = class_param_dep;
217         this.gen_class_inherit_dep = class_inherit_dep;
218         this.gen_class_member_dep = class_member_dep;
219         this.do_comp_by_file = do_comp_by_file;
220         this.do_gen_dot = gen_dot;
221         this.do_style_incl = style_incl;
222 
223         import std.path : baseName, buildPath, relativePath, stripExtension;
224 
225         this.file_classes = Path(buildPath(cast(string) output_dir,
226                 cast(string) file_prefix ~ "classes" ~ fileExt));
227         this.file_components = Path(buildPath(cast(string) output_dir,
228                 cast(string) file_prefix ~ "components" ~ fileExt));
229         this.file_style_output = Path(buildPath(cast(string) output_dir,
230                 cast(string) file_prefix ~ "style" ~ inclExt));
231         this.file_style = Path(relativePath(cast(string) file_prefix ~ "style" ~ inclExt,
232                 cast(string) output_dir));
233     }
234 
235     // -- Controller --
236 
237     bool doFile(in string filename, in string info) {
238         return fileFilter.match(filename, (string s, string type) {
239             logger.tracef("matcher --file-%s removed %s. Skipping", s, type);
240         });
241     }
242 
243     Flag!"genStyleInclFile" genStyleInclFile() {
244         import std.file : exists;
245 
246         return cast(Flag!"genStyleInclFile")(do_style_incl && !exists(cast(string) file_style));
247     }
248 
249     Path doComponentNameStrip(Path fname) {
250         import std.path : dirName;
251         import cpptooling.testdouble.header_filter : stripFile;
252 
253         if (do_comp_by_file) {
254             return Path(stripFile(cast(string) fname, comp_strip));
255         } else {
256             return Path(stripFile((cast(string) fname).dirName, comp_strip));
257         }
258     }
259 
260     // -- Parameters --
261 
262     Path getOutputDirectory() const {
263         return output_dir;
264     }
265 
266     Parameters.Files getFiles() const {
267         return Parameters.Files(file_classes, file_components, file_style, file_style_output);
268     }
269 
270     FilePrefix getFilePrefix() const {
271         return file_prefix;
272     }
273 
274     Flag!"genClassMethod" genClassMethod() const {
275         return gen_class_method;
276     }
277 
278     Flag!"genClassParamDependency" genClassParamDependency() const {
279         return gen_class_param_dep;
280     }
281 
282     Flag!"genClassInheritDependency" genClassInheritDependency() const {
283         return gen_class_inherit_dep;
284     }
285 
286     Flag!"genClassMemberDependency" genClassMemberDependency() const {
287         return gen_class_member_dep;
288     }
289 
290     Flag!"doStyleIncl" doStyleIncl() const {
291         return do_style_incl;
292     }
293 
294     Flag!"doGenDot" doGenDot() const {
295         return do_gen_dot;
296     }
297 
298     // -- Products --
299 
300     void putFile(Path fname, PlantumlRootModule root) {
301         fileData ~= FileData(fname, root.render());
302     }
303 
304     void putFile(Path fname, PlantumlModule pm) {
305         fileData ~= FileData(fname, pm.render());
306     }
307 }
308 
309 struct Lookup {
310     import cpptooling.data.symbol : Container, USRType;
311     import cpptooling.data : Location, LocationTag, TypeKind;
312 
313     private Container* container;
314 
315     auto kind(USRType usr) @safe {
316         return container.find!TypeKind(usr);
317     }
318 
319     auto location(USRType usr) @safe {
320         return container.find!LocationTag(usr);
321     }
322 }
323 
324 ExitStatusType genUml(PlantUMLFrontend variant, string[] in_cflags,
325         CompileCommandDB compile_db, Path[] inFiles, Flag!"skipFileError" skipFileError) {
326     import std.algorithm : map, joiner;
327     import std.array : array;
328     import std.conv : text;
329     import std.path : buildNormalizedPath, asAbsolutePath;
330     import std.typecons : Yes;
331 
332     import cpptooling.data : CppRoot;
333     import cpptooling.data.symbol : Container;
334     import libclang_ast.context : ClangContext;
335 
336     import dextool.clang : reduceMissingFiles;
337     import dextool.io : writeFileData;
338     import dextool.plugin.backend.plantuml : Generator, UMLVisitor,
339         UMLClassDiagram, UMLComponentDiagram, TransformToDiagram;
340     import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile;
341 
342     Container container;
343     auto generator = Generator(variant, variant, variant);
344 
345     // note how the transform is connected with destinations via the generator
346     // uml diagrams
347     auto transform = new TransformToDiagram!(Controller, Parameters, Lookup)(variant,
348             variant, Lookup(&container), generator.umlComponent, generator.umlClass);
349 
350     auto visitor = new UMLVisitor!(Controller, typeof(transform))(variant, transform, container);
351     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
352 
353     auto compDbRange() {
354         if (compile_db.empty) {
355             return fileRange(inFiles, Compiler("/usr/bin/c++"));
356         }
357         return compile_db.fileRange;
358     }
359 
360     auto fixedDb = compDbRange.parse(defaultCompilerFilter).addCompiler(Compiler("/usr/bin/c++"))
361         .addSystemIncludes.prependFlags(prependDefaultFlags(in_cflags, PreferLang.none)).array;
362 
363     auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array)
364         .reduceMissingFiles(fixedDb);
365 
366     if (!compile_db.empty && !limitRange.isMissingFilesEmpty) {
367         foreach (a; limitRange.missingFiles) {
368             logger.error("Unable to find any compiler flags for .", a);
369         }
370         return ExitStatusType.Errors;
371     }
372 
373     AbsolutePath[] unable_to_parse;
374 
375     foreach (entry; limitRange.range) {
376         auto analyze_status = analyzeFile(entry.cmd.absoluteFile,
377                 entry.flags.completeFlags, visitor, ctx);
378 
379         // compile error, let user decide how to proceed.
380         if (analyze_status == ExitStatusType.Errors && skipFileError) {
381             logger.errorf("Continue analyze...");
382             unable_to_parse ~= entry.cmd.absoluteFile;
383         } else if (analyze_status == ExitStatusType.Errors) {
384             return ExitStatusType.Errors;
385         }
386     }
387 
388     if (unable_to_parse.length > 0) {
389         // TODO be aware that no test exist for this logic
390         import std.ascii : newline;
391         import std.range : roundRobin, repeat;
392 
393         logger.errorf("Compile errors in the following files:\n%s\n",
394                 unable_to_parse.map!(a => (cast(string) a))
395                 .roundRobin(newline.repeat(unable_to_parse.length)).joiner().text);
396     }
397 
398     transform.finalize();
399     generator.process();
400 
401     debug {
402         logger.trace(container.toString);
403         logger.trace(generator.umlComponent.toString);
404         logger.trace(generator.umlClass.toString);
405     }
406 
407     return writeFileData(variant.fileData);
408 }