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 gmock_impl_file;
262         Path pre_incl_file;
263         Path post_incl_file;
264         Path config_file;
265         Path log_file;
266         CustomHeader custom_hdr;
267 
268         MainName main_name;
269         MainNs main_ns;
270         MainInterface main_if;
271         Flag!"Gmock" gmock;
272         Flag!"PreInclude" pre_incl;
273         Flag!"PostInclude" post_incl;
274         Flag!"locationAsComment" loc_as_comment;
275         Flag!"generateZeroGlobals" generate_zero_globals;
276 
277         string system_compiler;
278 
279         Nullable!XmlConfig xmlConfig;
280         CompileCommandFilter compiler_flag_filter;
281         FilterSymbol restrict_symbols;
282         FilterSymbol exclude_symbols;
283 
284         string[] exclude;
285         string[] include;
286         ReFilter fileFilter;
287 
288         /// Data produced by the generatore intented to be written to specified file.
289         FileData[] file_data;
290 
291         TestDoubleIncludes td_includes;
292     }
293 
294     static auto makeVariant(ref RawConfiguration args) {
295         // dfmt off
296         auto variant = new CTestDoubleVariant(
297                 MainFileName(args.mainFileName), Path(args.out_),
298                 regex(args.stripInclude))
299             .argPrefix(args.prefix)
300             .argMainName(args.mainName)
301             .argLocationAsComment(args.locationAsComment)
302             .argGmock(args.gmock)
303             .argPreInclude(args.generatePreInclude)
304             .argPostInclude(args.genPostInclude)
305             .argForceTestDoubleIncludes(args.testDoubleInclude)
306             .argFileExclude(args.fileExclude)
307             .argFileInclude(args.fileInclude)
308             .argCustomHeader(args.header, args.headerFile)
309             .argGenerateZeroGlobals(args.generateZeroGlobals)
310             .argXmlConfig(args.xmlConfig)
311             .systemCompiler(args.systemCompiler);
312         // dfmt on
313 
314         return variant;
315     }
316 
317     /** Design of c'tor.
318      *
319      * The c'tor has as paramters all the required configuration data.
320      * Assignment of members are used for optional configuration.
321      *
322      * Follows the design pattern "correct by construction".
323      *
324      * TODO document the parameters.
325      */
326     this(MainFileName main_fname, Path output_dir, Regex!char strip_incl) {
327         this.output_dir = output_dir;
328         this.td_includes = TestDoubleIncludes(strip_incl);
329 
330         import std.path : baseName, buildPath, stripExtension;
331 
332         string base_filename = cast(string) main_fname;
333 
334         this.main_file_hdr = Path(buildPath(cast(string) output_dir, base_filename ~ hdrExt));
335         this.main_file_impl = Path(buildPath(cast(string) output_dir, base_filename ~ implExt));
336         this.main_file_globals = Path(buildPath(cast(string) output_dir,
337                 base_filename ~ "_global" ~ implExt));
338         this.gmock_file = Path(buildPath(cast(string) output_dir, base_filename ~ "_gmock" ~ hdrExt));
339         this.gmock_impl_file = Path(buildPath(cast(string) output_dir,
340                 base_filename ~ "_gmock" ~ implExt));
341         this.pre_incl_file = Path(buildPath(cast(string) output_dir,
342                 base_filename ~ "_pre_includes" ~ hdrExt));
343         this.post_incl_file = Path(buildPath(cast(string) output_dir,
344                 base_filename ~ "_post_includes" ~ hdrExt));
345         this.config_file = Path(buildPath(output_dir, base_filename ~ "_config" ~ xmlExt));
346         this.log_file = Path(buildPath(output_dir, base_filename ~ "_log" ~ xmlExt));
347     }
348 
349     auto argFileExclude(string[] a) {
350         this.exclude = a;
351         fileFilter = ReFilter(include, exclude);
352         return this;
353     }
354 
355     auto argFileInclude(string[] a) {
356         this.include = a;
357         fileFilter = ReFilter(include, exclude);
358         return this;
359     }
360 
361     auto argPrefix(string s) {
362         this.prefix = StubPrefix(s);
363         return this;
364     }
365 
366     auto argMainName(string s) {
367         this.main_name = MainName(s);
368         this.main_ns = MainNs(s);
369         this.main_if = MainInterface("I_" ~ s);
370         return this;
371     }
372 
373     /// Force the includes to be those supplied by the user.
374     auto argForceTestDoubleIncludes(string[] a) {
375         if (a.length != 0) {
376             td_includes.forceIncludes(a);
377         }
378         return this;
379     }
380 
381     auto argCustomHeader(string header, string header_file) {
382         if (header.length != 0) {
383             this.custom_hdr = CustomHeader(header);
384         } else if (header_file.length != 0) {
385             import std.file : readText;
386 
387             string content = readText(header_file);
388             this.custom_hdr = CustomHeader(content);
389         }
390 
391         return this;
392     }
393 
394     auto argGmock(bool a) {
395         this.gmock = cast(Flag!"Gmock") a;
396         return this;
397     }
398 
399     auto argPreInclude(bool a) {
400         this.pre_incl = cast(Flag!"PreInclude") a;
401         return this;
402     }
403 
404     auto argPostInclude(bool a) {
405         this.post_incl = cast(Flag!"PostInclude") a;
406         return this;
407     }
408 
409     auto argLocationAsComment(bool a) {
410         this.loc_as_comment = cast(Flag!"locationAsComment") a;
411         return this;
412     }
413 
414     auto argGenerateZeroGlobals(bool value) {
415         this.generate_zero_globals = cast(Flag!"generateZeroGlobals") value;
416         return this;
417     }
418 
419     /** Ensure that the relevant information from the xml file is extracted.
420      *
421      * May overwrite information from the command line.
422      * TODO or should the command line have priority over the xml file?
423      */
424     auto argXmlConfig(Nullable!XmlConfig conf) {
425         import dextool.compilation_db : defaultCompilerFlagFilter;
426 
427         if (conf.isNull) {
428             compiler_flag_filter = CompileCommandFilter(defaultCompilerFlagFilter, 0);
429             return this;
430         }
431 
432         xmlConfig = conf;
433         compiler_flag_filter = CompileCommandFilter(conf.get.filterClangFlags,
434                 conf.get.skipCompilerArgs);
435         restrict_symbols = conf.get.restrictSymbols;
436         exclude_symbols = conf.get.excludeSymbols;
437 
438         return this;
439     }
440 
441     void processIncludes() {
442         td_includes.process();
443     }
444 
445     void finalizeIncludes() {
446         td_includes.finalize();
447     }
448 
449     /// Destination of the configuration file containing how the test double was generated.
450     Path getXmlConfigFile() {
451         return config_file;
452     }
453 
454     /** Destination of the xml log for how dextool was ran when generatinng the
455      * test double.
456      */
457     Path getXmlLog() {
458         return log_file;
459     }
460 
461     ref FilterSymbol getRestrictSymbols() {
462         return restrict_symbols;
463     }
464 
465     ref FilterSymbol getExcludeSymbols() {
466         return exclude_symbols;
467     }
468 
469     ref CompileCommandFilter getCompileCommandFilter() {
470         return compiler_flag_filter;
471     }
472 
473     /// Data produced by the generatore intented to be written to specified file.
474     ref FileData[] getProducedFiles() {
475         return file_data;
476     }
477 
478     void putFile(Path fname, string data) {
479         file_data ~= FileData(fname, data);
480     }
481 
482     auto systemCompiler(string a) {
483         this.system_compiler = a;
484         return this;
485     }
486 
487     // -- Controller --
488 
489     bool doFile(in string filename, in string info) {
490         return fileFilter.match(filename, (string s, string type) {
491             logger.tracef("matcher --file-%s removed %s. Skipping", s, type);
492         });
493     }
494 
495     bool doSymbol(string symbol) {
496         // fast path, assuming no symbol filter is the most common
497         if (!restrict_symbols.hasSymbols && !exclude_symbols.hasSymbols) {
498             return true;
499         }
500 
501         if (restrict_symbols.hasSymbols && exclude_symbols.hasSymbols) {
502             return restrict_symbols.contains(symbol) && !exclude_symbols.contains(symbol);
503         }
504 
505         if (restrict_symbols.hasSymbols) {
506             return restrict_symbols.contains(symbol);
507         }
508 
509         if (exclude_symbols.hasSymbols) {
510             return !exclude_symbols.contains(symbol);
511         }
512 
513         return true;
514     }
515 
516     bool doGoogleMock() {
517         return gmock;
518     }
519 
520     bool doPreIncludes() {
521         import std.file : exists;
522 
523         return pre_incl && !exists(cast(string) pre_incl_file);
524     }
525 
526     bool doIncludeOfPreIncludes() {
527         return pre_incl;
528     }
529 
530     bool doPostIncludes() {
531         import std.file : exists;
532 
533         return post_incl && !exists(cast(string) post_incl_file);
534     }
535 
536     bool doIncludeOfPostIncludes() {
537         return post_incl;
538     }
539 
540     bool doLocationAsComment() {
541         return loc_as_comment;
542     }
543 
544     // -- Parameters --
545 
546     Path[] getIncludes() {
547         return td_includes.includes.map!(a => Path(a)).array();
548     }
549 
550     Path getOutputDirectory() {
551         return output_dir;
552     }
553 
554     Parameters.Files getFiles() {
555         return Parameters.Files(main_file_hdr, main_file_impl, main_file_globals,
556                 gmock_file, gmock_impl_file, pre_incl_file, post_incl_file);
557     }
558 
559     MainName getMainName() {
560         return main_name;
561     }
562 
563     MainNs getMainNs() {
564         return main_ns;
565     }
566 
567     MainInterface getMainInterface() {
568         return main_if;
569     }
570 
571     StubPrefix getFilePrefix() {
572         return StubPrefix("");
573     }
574 
575     StubPrefix getArtifactPrefix() {
576         return prefix;
577     }
578 
579     DextoolVersion getToolVersion() {
580         import dextool.utility : dextoolVersion;
581 
582         return dextoolVersion;
583     }
584 
585     CustomHeader getCustomHeader() {
586         return custom_hdr;
587     }
588 
589     Flag!"generateZeroGlobals" generateZeroGlobals() {
590         return generate_zero_globals;
591     }
592 
593     Compiler getSystemCompiler() const {
594         return Compiler(system_compiler);
595     }
596 
597     Compiler getMissingFileCompiler() const {
598         if (system_compiler.empty)
599             return Compiler("/usr/bin/cc");
600         return getSystemCompiler();
601     }
602 
603     // -- Products --
604 
605     void putFile(Path fname, CppHModule hdr_data) {
606         file_data ~= FileData(fname, hdr_data.render());
607     }
608 
609     void putFile(Path fname, CppModule impl_data) {
610         file_data ~= FileData(fname, impl_data.render());
611     }
612 
613     void putLocation(Path fname, LocationType type) {
614         td_includes.put(fname, type);
615     }
616 }
617 
618 /// TODO refactor, doing too many things.
619 ExitStatusType genCstub(CTestDoubleVariant variant, string[] userCflags,
620         CompileCommandDB compile_db, Path[] inFiles) {
621     import std.typecons : Yes;
622 
623     import libclang_ast.context : ClangContext;
624     import dextool.clang : reduceMissingFiles;
625     import dextool.compilation_db : limitOrAllRange, parse, prependFlags,
626         addCompiler, replaceCompiler, addSystemIncludes, fileRange;
627     import dextool.io : writeFileData;
628     import dextool.plugin.ctestdouble.backend.cvariant : CVisitor, Generator;
629     import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile;
630 
631     scope visitor = new CVisitor(variant, variant);
632     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
633     auto generator = Generator(variant, variant, variant);
634 
635     auto compDbRange() {
636         if (compile_db.empty) {
637             return fileRange(inFiles, variant.getMissingFileCompiler);
638         }
639         return compile_db.fileRange;
640     }
641 
642     auto fixedDb = compDbRange.parse(variant.getCompileCommandFilter)
643         .addCompiler(variant.getMissingFileCompiler).replaceCompiler(
644                 variant.getSystemCompiler).addSystemIncludes.prependFlags(
645                 prependDefaultFlags(userCflags, PreferLang.c)).array;
646 
647     auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array)
648         .reduceMissingFiles(fixedDb);
649 
650     if (!compile_db.empty && !limitRange.isMissingFilesEmpty) {
651         foreach (a; limitRange.missingFiles) {
652             logger.error("Unable to find any compiler flags for ", a);
653         }
654         return ExitStatusType.Errors;
655     }
656 
657     foreach (pdata; limitRange.range) {
658         if (analyzeFile(pdata.cmd.absoluteFile, pdata.flags.completeFlags,
659                 visitor, ctx) == ExitStatusType.Errors) {
660             return ExitStatusType.Errors;
661         }
662 
663         generator.aggregate(visitor.root, visitor.container);
664         visitor.clearRoot;
665         variant.processIncludes;
666     }
667 
668     variant.finalizeIncludes;
669 
670     // Analyse and generate test double
671     generator.process(visitor.container);
672 
673     debug {
674         logger.trace(visitor);
675     }
676 
677     return writeFileData(variant.getProducedFiles);
678 }