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 logger = std.experimental.logger;
13 import std.algorithm : find, map;
14 import std.array : array, empty;
15 import std.range : drop;
16 import std.typecons : Nullable, Tuple;
17 
18 import cpptooling.type;
19 import dextool.compilation_db;
20 import dextool.type;
21 
22 import dextool.plugin.ctestdouble.backend.cvariant : Controller, Parameters, Products;
23 import dextool.plugin.ctestdouble.frontend.types;
24 import dextool.plugin.ctestdouble.frontend.xml;
25 
26 // workaround for ldc-1.1.0 and dmd-2.071.2
27 auto workaround_linker_error() {
28     import cpptooling.testdouble.header_filter : TestDoubleIncludes,
29         GenericTestDoubleIncludes, DummyPayload;
30 
31     return typeid(GenericTestDoubleIncludes!DummyPayload).toString();
32 }
33 
34 struct RawConfiguration {
35     Nullable!XmlConfig xmlConfig;
36 
37     string[] fileExclude;
38     string[] fileInclude;
39     string[] testDoubleInclude;
40     Path[] inFiles;
41     string[] cflags;
42     string[] compileDb;
43     string header;
44     string headerFile;
45     string mainName = "TestDouble";
46     string mainFileName = "test_double";
47     string prefix = "Test_";
48     string stripInclude;
49     string out_;
50     string config;
51     string systemCompiler = "/usr/bin/cc";
52     bool help;
53     bool shortPluginHelp;
54     bool gmock;
55     bool generatePreInclude;
56     bool genPostInclude;
57     bool locationAsComment;
58     bool generateZeroGlobals;
59     bool invalidXmlConfig;
60 
61     string[] originalFlags;
62 
63     void parse(string[] args) {
64         import std.getopt;
65 
66         originalFlags = args.dup;
67         string[] input;
68 
69         try {
70             bool no_zero_globals;
71             // dfmt off
72             getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help,
73                    "compile-db", &compileDb,
74                    "config", &config,
75                    "file-exclude", &fileExclude,
76                    "file-include", &fileInclude,
77                    "gen-post-incl", &genPostInclude,
78                    "gen-pre-incl", &generatePreInclude,
79                    "gmock", &gmock,
80                    "header", &header,
81                    "header-file", &headerFile,
82                    "in", &input,
83                    "loc-as-comment", &locationAsComment,
84                    "main", &mainName,
85                    "main-fname", &mainFileName,
86                    "no-zeroglobals", &no_zero_globals,
87                    "out", &out_,
88                    "prefix", &prefix,
89                    "short-plugin-help", &shortPluginHelp,
90                    "strip-incl", &stripInclude,
91                    "system-compiler", "Derive the system include paths from this compiler [default /usr/bin/cc]", &systemCompiler,
92                    "td-include", &testDoubleInclude);
93             // dfmt on
94             generateZeroGlobals = !no_zero_globals;
95         } catch (std.getopt.GetOptException ex) {
96             logger.error(ex.msg);
97             help = true;
98         }
99 
100         // default arguments
101         if (stripInclude.length == 0) {
102             stripInclude = r".*/(.*)";
103             logger.trace("--strip-incl: using default regex to strip include path (basename)");
104         }
105 
106         if (config.length != 0) {
107             xmlConfig = readRawConfig(Path(config));
108             if (xmlConfig.isNull) {
109                 invalidXmlConfig = true;
110             }
111         }
112 
113         inFiles = input.map!(a => Path(a)).array;
114 
115         // at this point args contain "what is left". What is interesting then is those after "--".
116         cflags = args.find("--").drop(1).array();
117     }
118 
119     void printHelp() {
120         import std.stdio : writefln;
121 
122         writefln("%s\n\n%s\n%s", ctestdouble_opt.usage,
123                 ctestdouble_opt.optional, ctestdouble_opt.others);
124     }
125 
126     void dump() {
127         // TODO remove this
128         logger.tracef("args:
129 --header            :%s
130 --header-file       :%s
131 --file-include      :%s
132 --prefix            :%s
133 --gmock             :%s
134 --out               :%s
135 --file-exclude      :%s
136 --main              :%s
137 --strip-incl        :%s
138 --main-fname        :%s
139 --in                :%s
140 --compile-db        :%s
141 --gen-post-incl     :%s
142 --gen-pre-incl      :%s
143 --help              :%s
144 --loc-as-comment    :%s
145 --td-include        :%s
146 --no-zeroglobals    :%s
147 --config            :%s
148 CFLAGS              :%s
149 
150 xmlConfig           :%s", header, headerFile, fileInclude, prefix, gmock,
151                 out_, fileExclude, mainName, stripInclude,
152                 mainFileName, inFiles, compileDb, genPostInclude, generatePreInclude, help, locationAsComment,
153                 testDoubleInclude, !generateZeroGlobals, config, cflags, xmlConfig);
154     }
155 }
156 
157 // dfmt off
158 static auto ctestdouble_opt = Tuple!(string, "usage", string, "optional", string, "others")(
159     "usage:
160  dextool ctestdouble [options] [--in=] [-- CFLAGS]",
161     // -------------
162     " --main=name        Used as part of interface, namespace etc [default: TestDouble]
163  --main-fname=n     Used as part of filename for generated files [default: test_double]
164  --prefix=p         Prefix used when generating test artifacts [default: Test_]
165  --strip-incl=r     A regexp used to strip the include paths
166  --gmock            Generate a gmock implementation of test double interface
167  --gen-pre-incl     Generate a pre include header file if it doesn't exist and use it
168  --gen-post-incl    Generate a post include header file if it doesn't exist and use it
169  --loc-as-comment   Generate a comment containing the location the symbol was derived from.
170                     Makes it easier to correctly define excludes/includes
171  --header=s         Prepend generated files with the string
172  --header-file=f    Prepend generated files with the header read from the file
173  --no-zeroglobals   Turn off generation of the default implementation that zeroes globals
174  --config=path      Use configuration file",
175     // -------------
176 "others:
177  --in=              Input file to parse
178  --out=dir          directory for generated files [default: ./]
179  --compile-db=      Retrieve compilation parameters from the file
180  --file-exclude=    Exclude files from generation matching the regex
181  --file-include=    Restrict the scope of the test double to those files
182                     matching the regex
183  --td-include=      User supplied includes used instead of those found
184 
185 REGEX
186 The regex syntax is found at http://dlang.org/phobos/std_regex.html
187 
188 Information about --strip-incl.
189   Default regexp is: .*/(.*)
190 
191   To allow the user to selectively extract parts of the include path dextool
192   applies the regex and then concatenates all the matcher groups found.  It is
193   turned into the replacement include path.
194 
195   Important to remember then is that this approach requires that at least one
196   matcher group exists.
197 
198 Information about --file-exclude.
199   The regex must fully match the filename the AST node is located in.
200   If it matches all data from the file is excluded from the generated code.
201 
202 Information about --file-include.
203   The regex must fully match the filename the AST node is located in.
204   Only symbols from files matching the include affect the generated test double.
205 
206 EXAMPLES
207 
208 Generate a simple C test double.
209   dextool ctestdouble functions.h
210 
211   Analyze and generate a test double for function prototypes and extern variables.
212   Both those found in functions.h and outside, aka via includes.
213 
214   The test double is written to ./test_double.hpp/.cpp.
215   The name of the interface is Test_Double.
216 
217 Generate a C test double excluding data from specified files.
218   dextool ctestdouble --file-exclude=/foo.h --file-exclude='functions.[h,c]' --out=outdata/ functions.h -- -DBAR -I/some/path
219 
220   The code analyzer (Clang) will be passed the compiler flags -DBAR and -I/some/path.
221   During generation declarations found in foo.h or functions.h will be excluded.
222 
223   The file holding the test double is written to directory outdata.
224 "
225 );
226 // dfmt on
227 
228 struct FileData {
229     import dextool.io : WriteStrategy;
230     import dextool.type : Path;
231 
232     Path filename;
233     string data;
234     WriteStrategy strategy;
235 }
236 
237 /** Test double generation of C code.
238  *
239  * TODO Describe the options.
240  */
241 class CTestDoubleVariant : Controller, Parameters, Products {
242     import std.regex : regex, Regex;
243     import std.typecons : Flag;
244     import my.filter : ReFilter;
245     import dsrcgen.cpp : CppModule, CppHModule;
246     import dextool.compilation_db : CompileCommandFilter;
247     import cpptooling.testdouble.header_filter : TestDoubleIncludes, LocationType;
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         string system_compiler;
277 
278         Nullable!XmlConfig xmlConfig;
279         CompileCommandFilter compiler_flag_filter;
280         FilterSymbol restrict_symbols;
281         FilterSymbol exclude_symbols;
282 
283         string[] exclude;
284         string[] include;
285         ReFilter fileFilter;
286 
287         /// Data produced by the generatore intented to be written to specified file.
288         FileData[] file_data;
289 
290         TestDoubleIncludes td_includes;
291     }
292 
293     static auto makeVariant(ref RawConfiguration args) {
294         // dfmt off
295         auto variant = new CTestDoubleVariant(
296                 MainFileName(args.mainFileName), Path(args.out_),
297                 regex(args.stripInclude))
298             .argPrefix(args.prefix)
299             .argMainName(args.mainName)
300             .argLocationAsComment(args.locationAsComment)
301             .argGmock(args.gmock)
302             .argPreInclude(args.generatePreInclude)
303             .argPostInclude(args.genPostInclude)
304             .argForceTestDoubleIncludes(args.testDoubleInclude)
305             .argFileExclude(args.fileExclude)
306             .argFileInclude(args.fileInclude)
307             .argCustomHeader(args.header, args.headerFile)
308             .argGenerateZeroGlobals(args.generateZeroGlobals)
309             .argXmlConfig(args.xmlConfig)
310             .systemCompiler(args.systemCompiler);
311         // dfmt on
312 
313         return variant;
314     }
315 
316     /** Design of c'tor.
317      *
318      * The c'tor has as paramters all the required configuration data.
319      * Assignment of members are used for optional configuration.
320      *
321      * Follows the design pattern "correct by construction".
322      *
323      * TODO document the parameters.
324      */
325     this(MainFileName main_fname, Path output_dir, Regex!char strip_incl) {
326         this.output_dir = output_dir;
327         this.td_includes = TestDoubleIncludes(strip_incl);
328 
329         import std.path : baseName, buildPath, stripExtension;
330 
331         string base_filename = cast(string) main_fname;
332 
333         this.main_file_hdr = Path(buildPath(cast(string) output_dir, base_filename ~ hdrExt));
334         this.main_file_impl = Path(buildPath(cast(string) output_dir, base_filename ~ implExt));
335         this.main_file_globals = Path(buildPath(cast(string) output_dir,
336                 base_filename ~ "_global" ~ implExt));
337         this.gmock_file = Path(buildPath(cast(string) output_dir, base_filename ~ "_gmock" ~ hdrExt));
338         this.pre_incl_file = Path(buildPath(cast(string) output_dir,
339                 base_filename ~ "_pre_includes" ~ hdrExt));
340         this.post_incl_file = Path(buildPath(cast(string) output_dir,
341                 base_filename ~ "_post_includes" ~ hdrExt));
342         this.config_file = Path(buildPath(output_dir, base_filename ~ "_config" ~ xmlExt));
343         this.log_file = Path(buildPath(output_dir, base_filename ~ "_log" ~ xmlExt));
344     }
345 
346     auto argFileExclude(string[] a) {
347         this.exclude = a;
348         fileFilter = ReFilter(include, exclude);
349         return this;
350     }
351 
352     auto argFileInclude(string[] a) {
353         this.include = a;
354         fileFilter = ReFilter(include, exclude);
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, 0);
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     auto systemCompiler(string a) {
480         this.system_compiler = a;
481         return this;
482     }
483 
484     // -- Controller --
485 
486     bool doFile(in string filename, in string info) {
487         return fileFilter.match(filename, (string s, string type) {
488             logger.tracef("matcher --file-%s removed %s. Skipping", s, type);
489         });
490     }
491 
492     bool doSymbol(string symbol) {
493         // fast path, assuming no symbol filter is the most common
494         if (!restrict_symbols.hasSymbols && !exclude_symbols.hasSymbols) {
495             return true;
496         }
497 
498         if (restrict_symbols.hasSymbols && exclude_symbols.hasSymbols) {
499             return restrict_symbols.contains(symbol) && !exclude_symbols.contains(symbol);
500         }
501 
502         if (restrict_symbols.hasSymbols) {
503             return restrict_symbols.contains(symbol);
504         }
505 
506         if (exclude_symbols.hasSymbols) {
507             return !exclude_symbols.contains(symbol);
508         }
509 
510         return true;
511     }
512 
513     bool doGoogleMock() {
514         return gmock;
515     }
516 
517     bool doPreIncludes() {
518         import std.file : exists;
519 
520         return pre_incl && !exists(cast(string) pre_incl_file);
521     }
522 
523     bool doIncludeOfPreIncludes() {
524         return pre_incl;
525     }
526 
527     bool doPostIncludes() {
528         import std.file : exists;
529 
530         return post_incl && !exists(cast(string) post_incl_file);
531     }
532 
533     bool doIncludeOfPostIncludes() {
534         return post_incl;
535     }
536 
537     bool doLocationAsComment() {
538         return loc_as_comment;
539     }
540 
541     // -- Parameters --
542 
543     Path[] getIncludes() {
544         return td_includes.includes.map!(a => Path(a)).array();
545     }
546 
547     Path getOutputDirectory() {
548         return output_dir;
549     }
550 
551     Parameters.Files getFiles() {
552         return Parameters.Files(main_file_hdr, main_file_impl,
553                 main_file_globals, gmock_file, pre_incl_file, post_incl_file);
554     }
555 
556     MainName getMainName() {
557         return main_name;
558     }
559 
560     MainNs getMainNs() {
561         return main_ns;
562     }
563 
564     MainInterface getMainInterface() {
565         return main_if;
566     }
567 
568     StubPrefix getFilePrefix() {
569         return StubPrefix("");
570     }
571 
572     StubPrefix getArtifactPrefix() {
573         return prefix;
574     }
575 
576     DextoolVersion getToolVersion() {
577         import dextool.utility : dextoolVersion;
578 
579         return dextoolVersion;
580     }
581 
582     CustomHeader getCustomHeader() {
583         return custom_hdr;
584     }
585 
586     Flag!"generateZeroGlobals" generateZeroGlobals() {
587         return generate_zero_globals;
588     }
589 
590     Compiler getSystemCompiler() const {
591         return Compiler(system_compiler);
592     }
593 
594     Compiler getMissingFileCompiler() const {
595         if (system_compiler.empty)
596             return Compiler("/usr/bin/cc");
597         return getSystemCompiler();
598     }
599 
600     // -- Products --
601 
602     void putFile(Path fname, CppHModule hdr_data) {
603         file_data ~= FileData(fname, hdr_data.render());
604     }
605 
606     void putFile(Path fname, CppModule impl_data) {
607         file_data ~= FileData(fname, impl_data.render());
608     }
609 
610     void putLocation(Path fname, LocationType type) {
611         td_includes.put(fname, type);
612     }
613 }
614 
615 /// TODO refactor, doing too many things.
616 ExitStatusType genCstub(CTestDoubleVariant variant, string[] userCflags,
617         CompileCommandDB compile_db, Path[] inFiles) {
618     import std.typecons : Yes;
619 
620     import libclang_ast.context : ClangContext;
621     import dextool.clang : reduceMissingFiles;
622     import dextool.compilation_db : limitOrAllRange, parse, prependFlags,
623         addCompiler, replaceCompiler, addSystemIncludes, fileRange;
624     import dextool.io : writeFileData;
625     import dextool.plugin.ctestdouble.backend.cvariant : CVisitor, Generator;
626     import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile;
627 
628     auto visitor = new CVisitor(variant, variant);
629     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
630     auto generator = Generator(variant, variant, variant);
631 
632     auto compDbRange() {
633         if (compile_db.empty) {
634             return fileRange(inFiles, variant.getMissingFileCompiler);
635         }
636         return compile_db.fileRange;
637     }
638 
639     auto fixedDb = compDbRange.parse(variant.getCompileCommandFilter)
640         .addCompiler(variant.getMissingFileCompiler).replaceCompiler(
641                 variant.getSystemCompiler).addSystemIncludes.prependFlags(
642                 prependDefaultFlags(userCflags, PreferLang.c)).array;
643 
644     auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array)
645         .reduceMissingFiles(fixedDb);
646 
647     if (!compile_db.empty && !limitRange.isMissingFilesEmpty) {
648         foreach (a; limitRange.missingFiles) {
649             logger.error("Unable to find any compiler flags for ", a);
650         }
651         return ExitStatusType.Errors;
652     }
653 
654     foreach (pdata; limitRange.range) {
655         if (analyzeFile(pdata.cmd.absoluteFile, pdata.flags.completeFlags,
656                 visitor, ctx) == ExitStatusType.Errors) {
657             return ExitStatusType.Errors;
658         }
659 
660         generator.aggregate(visitor.root, visitor.container);
661         visitor.clearRoot;
662         variant.processIncludes;
663     }
664 
665     variant.finalizeIncludes;
666 
667     // Analyse and generate test double
668     generator.process(visitor.container);
669 
670     debug {
671         logger.trace(visitor);
672     }
673 
674     return writeFileData(variant.getProducedFiles);
675 }