1 /**
2 Copyright: Copyright (c) 2015-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 module dextool.plugin.ctestdouble.frontend.ctestdouble;
11 
12 import std.typecons : Nullable;
13 
14 import logger = std.experimental.logger;
15 
16 import dextool.compilation_db;
17 import dextool.type;
18 
19 import dextool.plugin.types;
20 import dextool.plugin.ctestdouble.backend.cvariant : Controller, Parameters, Products;
21 import dextool.plugin.ctestdouble.frontend.types;
22 import dextool.plugin.ctestdouble.frontend.xml;
23 
24 // workaround for ldc-1.1.0 and dmd-2.071.2
25 auto workaround_linker_error() {
26     import cpptooling.testdouble.header_filter : TestDoubleIncludes,
27         GenericTestDoubleIncludes, DummyPayload;
28 
29     return typeid(GenericTestDoubleIncludes!DummyPayload).toString();
30 }
31 
32 struct RawConfiguration {
33     Nullable!XmlConfig xmlConfig;
34 
35     string[] fileExclude;
36     string[] fileRestrict;
37     string[] testDoubleInclude;
38     string[] inFiles;
39     string[] cflags;
40     string[] compileDb;
41     string header;
42     string headerFile;
43     string mainName = "TestDouble";
44     string mainFileName = "test_double";
45     string prefix = "Test_";
46     string stripInclude;
47     string out_;
48     string config;
49     bool help;
50     bool shortPluginHelp;
51     bool gmock;
52     bool generatePreInclude;
53     bool genPostInclude;
54     bool locationAsComment;
55     bool generateZeroGlobals;
56     bool invalidXmlConfig;
57 
58     string[] originalFlags;
59 
60     void parse(string[] args) {
61         import std.getopt;
62 
63         originalFlags = args.dup;
64 
65         try {
66             bool no_zero_globals;
67             // dfmt off
68             getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help,
69                    "short-plugin-help", &shortPluginHelp,
70                    "main", &mainName,
71                    "main-fname", &mainFileName,
72                    "out", &out_,
73                    "compile-db", &compileDb,
74                    "no-zeroglobals", &no_zero_globals,
75                    "prefix", &prefix,
76                    "strip-incl", &stripInclude,
77                    "header", &header,
78                    "header-file", &headerFile,
79                    "gmock", &gmock,
80                    "gen-pre-incl", &generatePreInclude,
81                    "gen-post-incl", &genPostInclude,
82                    "loc-as-comment", &locationAsComment,
83                    "td-include", &testDoubleInclude,
84                    "file-exclude", &fileExclude,
85                    "file-restrict", &fileRestrict,
86                    "in", &inFiles,
87                    "config", &config);
88             // dfmt on
89             generateZeroGlobals = !no_zero_globals;
90         } catch (std.getopt.GetOptException ex) {
91             logger.error(ex.msg);
92             help = true;
93         }
94 
95         // default arguments
96         if (stripInclude.length == 0) {
97             stripInclude = r".*/(.*)";
98             logger.trace("--strip-incl: using default regex to strip include path (basename)");
99         }
100 
101         if (config.length != 0) {
102             xmlConfig = readRawConfig(FileName(config));
103             if (xmlConfig.isNull) {
104                 invalidXmlConfig = true;
105             }
106         }
107 
108         import std.algorithm : find;
109         import std.array : array;
110         import std.range : drop;
111 
112         // at this point args contain "what is left". What is interesting then is those after "--".
113         cflags = args.find("--").drop(1).array();
114     }
115 
116     void printHelp() {
117         import std.stdio : writefln;
118 
119         writefln("%s\n\n%s\n%s", ctestdouble_opt.usage,
120                 ctestdouble_opt.optional, ctestdouble_opt.others);
121     }
122 
123     void dump() {
124         // TODO remove this
125         logger.tracef("args:
126 --header            :%s
127 --header-file       :%s
128 --file-restrict     :%s
129 --prefix            :%s
130 --gmock             :%s
131 --out               :%s
132 --file-exclude      :%s
133 --main              :%s
134 --strip-incl        :%s
135 --main-fname        :%s
136 --in                :%s
137 --compile-db        :%s
138 --gen-post-incl     :%s
139 --gen-pre-incl      :%s
140 --help              :%s
141 --loc-as-comment    :%s
142 --td-include        :%s
143 --no-zeroglobals    :%s
144 --config            :%s
145 CFLAGS              :%s
146 
147 xmlConfig           :%s", header, headerFile, fileRestrict, prefix, gmock,
148                 out_, fileExclude, mainName, stripInclude,
149                 mainFileName, inFiles, compileDb, genPostInclude, generatePreInclude, help, locationAsComment,
150                 testDoubleInclude, !generateZeroGlobals, config, cflags, xmlConfig);
151     }
152 }
153 
154 // dfmt off
155 static auto ctestdouble_opt = CliOptionParts(
156     "usage:
157  dextool ctestdouble [options] [--in=] [-- CFLAGS]",
158     // -------------
159     " --main=name        Used as part of interface, namespace etc [default: TestDouble]
160  --main-fname=n     Used as part of filename for generated files [default: test_double]
161  --prefix=p         Prefix used when generating test artifacts [default: Test_]
162  --strip-incl=r     A regexp used to strip the include paths
163  --gmock            Generate a gmock implementation of test double interface
164  --gen-pre-incl     Generate a pre include header file if it doesn't exist and use it
165  --gen-post-incl    Generate a post include header file if it doesn't exist and use it
166  --loc-as-comment   Generate a comment containing the location the symbol was derived from.
167                     Makes it easier to correctly define excludes/restricts
168  --header=s         Prepend generated files with the string
169  --header-file=f    Prepend generated files with the header read from the file
170  --no-zeroglobals   Turn off generation of the default implementation that zeroes globals
171  --config=path      Use configuration file",
172     // -------------
173 "others:
174  --in=              Input file to parse
175  --out=dir          directory for generated files [default: ./]
176  --compile-db=      Retrieve compilation parameters from the file
177  --file-exclude=    Exclude files from generation matching the regex
178  --file-restrict=   Restrict the scope of the test double to those files
179                     matching the regex
180  --td-include=      User supplied includes used instead of those found
181 
182 REGEX
183 The regex syntax is found at http://dlang.org/phobos/std_regex.html
184 
185 Information about --strip-incl.
186   Default regexp is: .*/(.*)
187 
188   To allow the user to selectively extract parts of the include path dextool
189   applies the regex and then concatenates all the matcher groups found.  It is
190   turned into the replacement include path.
191 
192   Important to remember then is that this approach requires that at least one
193   matcher group exists.
194 
195 Information about --file-exclude.
196   The regex must fully match the filename the AST node is located in.
197   If it matches all data from the file is excluded from the generated code.
198 
199 Information about --file-restrict.
200   The regex must fully match the filename the AST node is located in.
201   Only symbols from files matching the restrict affect the generated test double.
202 
203 EXAMPLES
204 
205 Generate a simple C test double.
206   dextool ctestdouble functions.h
207 
208   Analyze and generate a test double for function prototypes and extern variables.
209   Both those found in functions.h and outside, aka via includes.
210 
211   The test double is written to ./test_double.hpp/.cpp.
212   The name of the interface is Test_Double.
213 
214 Generate a C test double excluding data from specified files.
215   dextool ctestdouble --file-exclude=/foo.h --file-exclude='functions.[h,c]' --out=outdata/ functions.h -- -DBAR -I/some/path
216 
217   The code analyzer (Clang) will be passed the compiler flags -DBAR and -I/some/path.
218   During generation declarations found in foo.h or functions.h will be excluded.
219 
220   The file holding the test double is written to directory outdata.
221 "
222 );
223 // dfmt on
224 
225 struct FileData {
226     import dextool.type : FileName, WriteStrategy;
227 
228     FileName filename;
229     string data;
230     WriteStrategy strategy;
231 }
232 
233 /** Test double generation of C code.
234  *
235  * TODO Describe the options.
236  */
237 class CTestDoubleVariant : Controller, Parameters, Products {
238     import std.regex : regex, Regex;
239     import std.typecons : Flag;
240     import dextool.compilation_db : CompileCommandFilter;
241     import dextool.type : StubPrefix, FileName, DirName;
242     import cpptooling.testdouble.header_filter : TestDoubleIncludes, LocationType;
243     import dsrcgen.cpp : CppModule, CppHModule;
244 
245     private {
246         static const hdrExt = ".hpp";
247         static const implExt = ".cpp";
248         static const xmlExt = ".xml";
249 
250         StubPrefix prefix;
251 
252         DirName output_dir;
253         FileName main_file_hdr;
254         FileName main_file_impl;
255         FileName main_file_globals;
256         FileName gmock_file;
257         FileName pre_incl_file;
258         FileName post_incl_file;
259         FileName config_file;
260         FileName log_file;
261         CustomHeader custom_hdr;
262 
263         MainName main_name;
264         MainNs main_ns;
265         MainInterface main_if;
266         Flag!"Gmock" gmock;
267         Flag!"PreInclude" pre_incl;
268         Flag!"PostInclude" post_incl;
269         Flag!"locationAsComment" loc_as_comment;
270         Flag!"generateZeroGlobals" generate_zero_globals;
271 
272         Nullable!XmlConfig xmlConfig;
273         CompileCommandFilter compiler_flag_filter;
274         FilterSymbol restrict_symbols;
275         FilterSymbol exclude_symbols;
276 
277         Regex!char[] exclude;
278         Regex!char[] restrict;
279 
280         /// Data produced by the generatore intented to be written to specified file.
281         FileData[] file_data;
282 
283         TestDoubleIncludes td_includes;
284     }
285 
286     static auto makeVariant(ref RawConfiguration args) {
287         // dfmt off
288         auto variant = new CTestDoubleVariant(
289                 MainFileName(args.mainFileName), DirName(args.out_),
290                 regex(args.stripInclude))
291             .argPrefix(args.prefix)
292             .argMainName(args.mainName)
293             .argLocationAsComment(args.locationAsComment)
294             .argGmock(args.gmock)
295             .argPreInclude(args.generatePreInclude)
296             .argPostInclude(args.genPostInclude)
297             .argForceTestDoubleIncludes(args.testDoubleInclude)
298             .argFileExclude(args.fileExclude)
299             .argFileRestrict(args.fileRestrict)
300             .argCustomHeader(args.header, args.headerFile)
301             .argGenerateZeroGlobals(args.generateZeroGlobals)
302             .argXmlConfig(args.xmlConfig);
303         // dfmt on
304 
305         return variant;
306     }
307 
308     /** Design of c'tor.
309      *
310      * The c'tor has as paramters all the required configuration data.
311      * Assignment of members are used for optional configuration.
312      *
313      * Follows the design pattern "correct by construction".
314      *
315      * TODO document the parameters.
316      */
317     this(MainFileName main_fname, DirName output_dir, Regex!char strip_incl) {
318         this.output_dir = output_dir;
319         this.td_includes = TestDoubleIncludes(strip_incl);
320 
321         import std.path : baseName, buildPath, stripExtension;
322 
323         string base_filename = cast(string) main_fname;
324 
325         this.main_file_hdr = FileName(buildPath(cast(string) output_dir, base_filename ~ hdrExt));
326         this.main_file_impl = FileName(buildPath(cast(string) output_dir, base_filename ~ implExt));
327         this.main_file_globals = FileName(buildPath(cast(string) output_dir,
328                 base_filename ~ "_global" ~ implExt));
329         this.gmock_file = FileName(buildPath(cast(string) output_dir,
330                 base_filename ~ "_gmock" ~ hdrExt));
331         this.pre_incl_file = FileName(buildPath(cast(string) output_dir,
332                 base_filename ~ "_pre_includes" ~ hdrExt));
333         this.post_incl_file = FileName(buildPath(cast(string) output_dir,
334                 base_filename ~ "_post_includes" ~ hdrExt));
335         this.config_file = FileName(buildPath(output_dir, base_filename ~ "_config" ~ xmlExt));
336         this.log_file = FileName(buildPath(output_dir, base_filename ~ "_log" ~ xmlExt));
337     }
338 
339     auto argFileExclude(string[] a) {
340         import std.array : array;
341         import std.algorithm : map;
342 
343         this.exclude = a.map!(a => regex(a)).array();
344         return this;
345     }
346 
347     auto argFileRestrict(string[] a) {
348         import std.array : array;
349         import std.algorithm : map;
350 
351         this.restrict = a.map!(a => regex(a)).array();
352         return this;
353     }
354 
355     auto argPrefix(string s) {
356         this.prefix = StubPrefix(s);
357         return this;
358     }
359 
360     auto argMainName(string s) {
361         this.main_name = MainName(s);
362         this.main_ns = MainNs(s);
363         this.main_if = MainInterface("I_" ~ s);
364         return this;
365     }
366 
367     /// Force the includes to be those supplied by the user.
368     auto argForceTestDoubleIncludes(string[] a) {
369         if (a.length != 0) {
370             td_includes.forceIncludes(a);
371         }
372         return this;
373     }
374 
375     auto argCustomHeader(string header, string header_file) {
376         if (header.length != 0) {
377             this.custom_hdr = CustomHeader(header);
378         } else if (header_file.length != 0) {
379             import std.file : readText;
380 
381             string content = readText(header_file);
382             this.custom_hdr = CustomHeader(content);
383         }
384 
385         return this;
386     }
387 
388     auto argGmock(bool a) {
389         this.gmock = cast(Flag!"Gmock") a;
390         return this;
391     }
392 
393     auto argPreInclude(bool a) {
394         this.pre_incl = cast(Flag!"PreInclude") a;
395         return this;
396     }
397 
398     auto argPostInclude(bool a) {
399         this.post_incl = cast(Flag!"PostInclude") a;
400         return this;
401     }
402 
403     auto argLocationAsComment(bool a) {
404         this.loc_as_comment = cast(Flag!"locationAsComment") a;
405         return this;
406     }
407 
408     auto argGenerateZeroGlobals(bool value) {
409         this.generate_zero_globals = cast(Flag!"generateZeroGlobals") value;
410         return this;
411     }
412 
413     /** Ensure that the relevant information from the xml file is extracted.
414      *
415      * May overwrite information from the command line.
416      * TODO or should the command line have priority over the xml file?
417      */
418     auto argXmlConfig(Nullable!XmlConfig conf) {
419         import dextool.compilation_db : defaultCompilerFlagFilter;
420 
421         if (conf.isNull) {
422             compiler_flag_filter = CompileCommandFilter(defaultCompilerFlagFilter, 1);
423             return this;
424         }
425 
426         xmlConfig = conf;
427         compiler_flag_filter = CompileCommandFilter(conf.filterClangFlags, conf.skipCompilerArgs);
428         restrict_symbols = conf.restrictSymbols;
429         exclude_symbols = conf.excludeSymbols;
430 
431         return this;
432     }
433 
434     void processIncludes() {
435         td_includes.process();
436     }
437 
438     void finalizeIncludes() {
439         td_includes.finalize();
440     }
441 
442     /// Destination of the configuration file containing how the test double was generated.
443     FileName getXmlConfigFile() {
444         return config_file;
445     }
446 
447     /** Destination of the xml log for how dextool was ran when generatinng the
448      * test double.
449      */
450     FileName getXmlLog() {
451         return log_file;
452     }
453 
454     ref FilterSymbol getRestrictSymbols() {
455         return restrict_symbols;
456     }
457 
458     ref FilterSymbol getExcludeSymbols() {
459         return exclude_symbols;
460     }
461 
462     ref CompileCommandFilter getCompileCommandFilter() {
463         return compiler_flag_filter;
464     }
465 
466     /// Data produced by the generatore intented to be written to specified file.
467     ref FileData[] getProducedFiles() {
468         return file_data;
469     }
470 
471     void putFile(FileName fname, string data) {
472         file_data ~= FileData(fname, data);
473     }
474 
475     // -- Controller --
476 
477     bool doFile(in string filename, in string info) {
478         import dextool.plugin.regex_matchers : matchAny;
479 
480         bool restrict_pass = true;
481         bool exclude_pass = true;
482 
483         if (restrict.length > 0) {
484             restrict_pass = matchAny(filename, restrict);
485             debug {
486                 logger.tracef(!restrict_pass, "--file-restrict skipping %s", info);
487             }
488         }
489 
490         if (exclude.length > 0) {
491             exclude_pass = !matchAny(filename, exclude);
492             debug {
493                 logger.tracef(!exclude_pass, "--file-exclude skipping %s", info);
494             }
495         }
496 
497         return restrict_pass && exclude_pass;
498     }
499 
500     bool doSymbol(string symbol) {
501         // fast path, assuming no symbol filter is the most common
502         if (!restrict_symbols.hasSymbols && !exclude_symbols.hasSymbols) {
503             return true;
504         }
505 
506         if (restrict_symbols.hasSymbols && exclude_symbols.hasSymbols) {
507             return restrict_symbols.contains(symbol) && !exclude_symbols.contains(symbol);
508         }
509 
510         if (restrict_symbols.hasSymbols) {
511             return restrict_symbols.contains(symbol);
512         }
513 
514         if (exclude_symbols.hasSymbols) {
515             return !exclude_symbols.contains(symbol);
516         }
517 
518         return true;
519     }
520 
521     bool doGoogleMock() {
522         return gmock;
523     }
524 
525     bool doPreIncludes() {
526         import std.file : exists;
527 
528         return pre_incl && !exists(cast(string) pre_incl_file);
529     }
530 
531     bool doIncludeOfPreIncludes() {
532         return pre_incl;
533     }
534 
535     bool doPostIncludes() {
536         import std.file : exists;
537 
538         return post_incl && !exists(cast(string) post_incl_file);
539     }
540 
541     bool doIncludeOfPostIncludes() {
542         return post_incl;
543     }
544 
545     bool doLocationAsComment() {
546         return loc_as_comment;
547     }
548 
549     // -- Parameters --
550 
551     FileName[] getIncludes() {
552         import std.algorithm : map;
553         import std.array : array;
554 
555         return td_includes.includes.map!(a => FileName(a)).array();
556     }
557 
558     DirName getOutputDirectory() {
559         return output_dir;
560     }
561 
562     Parameters.Files getFiles() {
563         return Parameters.Files(main_file_hdr, main_file_impl,
564                 main_file_globals, gmock_file, pre_incl_file, post_incl_file);
565     }
566 
567     MainName getMainName() {
568         return main_name;
569     }
570 
571     MainNs getMainNs() {
572         return main_ns;
573     }
574 
575     MainInterface getMainInterface() {
576         return main_if;
577     }
578 
579     StubPrefix getFilePrefix() {
580         return StubPrefix("");
581     }
582 
583     StubPrefix getArtifactPrefix() {
584         return prefix;
585     }
586 
587     DextoolVersion getToolVersion() {
588         import dextool.utility : dextoolVersion;
589 
590         return dextoolVersion;
591     }
592 
593     CustomHeader getCustomHeader() {
594         return custom_hdr;
595     }
596 
597     Flag!"generateZeroGlobals" generateZeroGlobals() {
598         return generate_zero_globals;
599     }
600 
601     // -- Products --
602 
603     void putFile(FileName fname, CppHModule hdr_data) {
604         file_data ~= FileData(fname, hdr_data.render());
605     }
606 
607     void putFile(FileName fname, CppModule impl_data) {
608         file_data ~= FileData(fname, impl_data.render());
609     }
610 
611     void putLocation(FileName fname, LocationType type) {
612         td_includes.put(fname, type);
613     }
614 }
615 
616 /// TODO refactor, doing too many things.
617 ExitStatusType genCstub(CTestDoubleVariant variant, in string[] in_cflags,
618         CompileCommandDB compile_db, InFiles in_files) {
619     import std.typecons : Yes;
620 
621     import dextool.clang : findFlags;
622     import dextool.compilation_db : ParseData = SearchResult;
623     import cpptooling.analyzer.clang.context : ClangContext;
624     import dextool.io : writeFileData;
625     import dextool.plugin.ctestdouble.backend.cvariant : CVisitor, Generator;
626     import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile;
627 
628     const user_cflags = prependDefaultFlags(in_cflags, PreferLang.c);
629     const total_files = in_files.length;
630     auto visitor = new CVisitor(variant, variant);
631     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
632     auto generator = Generator(variant, variant, variant);
633 
634     foreach (idx, in_file; in_files) {
635         logger.infof("File %d/%d ", idx + 1, total_files);
636         ParseData pdata;
637 
638         // TODO duplicate code in c, c++ and plantuml. Fix it.
639         if (compile_db.length > 0) {
640             auto tmp = compile_db.findFlags(FileName(in_file), user_cflags,
641                     variant.getCompileCommandFilter);
642             if (tmp.isNull) {
643                 return ExitStatusType.Errors;
644             }
645             pdata = tmp.get;
646         } else {
647             pdata.flags.prependCflags(user_cflags.dup);
648             pdata.absoluteFile = AbsolutePath(FileName(in_file));
649         }
650 
651         if (analyzeFile(pdata.absoluteFile, pdata.cflags, visitor, ctx) == ExitStatusType.Errors) {
652             return ExitStatusType.Errors;
653         }
654 
655         generator.aggregate(visitor.root, visitor.container);
656         visitor.clearRoot;
657         variant.processIncludes;
658     }
659 
660     variant.finalizeIncludes;
661 
662     // Analyse and generate test double
663     generator.process(visitor.container);
664 
665     debug {
666         logger.trace(visitor);
667     }
668 
669     return writeFileData(variant.getProducedFiles);
670 }