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