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