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;
17 
18 import cpptooling.type;
19 import dextool.compilation_db;
20 import dextool.type;
21 
22 import dextool.plugin.types;
23 import dextool.plugin.ctestdouble.backend.cvariant : Controller, Parameters, Products;
24 import dextool.plugin.ctestdouble.frontend.types;
25 import dextool.plugin.ctestdouble.frontend.xml;
26 
27 // workaround for ldc-1.1.0 and dmd-2.071.2
28 auto workaround_linker_error() {
29     import cpptooling.testdouble.header_filter : TestDoubleIncludes,
30         GenericTestDoubleIncludes, DummyPayload;
31 
32     return typeid(GenericTestDoubleIncludes!DummyPayload).toString();
33 }
34 
35 struct RawConfiguration {
36     Nullable!XmlConfig xmlConfig;
37 
38     string[] fileExclude;
39     string[] fileRestrict;
40     string[] testDoubleInclude;
41     Path[] inFiles;
42     string[] cflags;
43     string[] compileDb;
44     string header;
45     string headerFile;
46     string mainName = "TestDouble";
47     string mainFileName = "test_double";
48     string prefix = "Test_";
49     string stripInclude;
50     string out_;
51     string config;
52     string systemCompiler = "/usr/bin/cc";
53     bool help;
54     bool shortPluginHelp;
55     bool gmock;
56     bool generatePreInclude;
57     bool genPostInclude;
58     bool locationAsComment;
59     bool generateZeroGlobals;
60     bool invalidXmlConfig;
61 
62     string[] originalFlags;
63 
64     void parse(string[] args) {
65         import std.getopt;
66 
67         originalFlags = args.dup;
68         string[] input;
69 
70         try {
71             bool no_zero_globals;
72             // dfmt off
73             getopt(args, std.getopt.config.keepEndOfOptions, "h|help", &help,
74                    "compile-db", &compileDb,
75                    "config", &config,
76                    "file-exclude", &fileExclude,
77                    "file-restrict", &fileRestrict,
78                    "gen-post-incl", &genPostInclude,
79                    "gen-pre-incl", &generatePreInclude,
80                    "gmock", &gmock,
81                    "header", &header,
82                    "header-file", &headerFile,
83                    "in", &input,
84                    "loc-as-comment", &locationAsComment,
85                    "main", &mainName,
86                    "main-fname", &mainFileName,
87                    "no-zeroglobals", &no_zero_globals,
88                    "out", &out_,
89                    "prefix", &prefix,
90                    "short-plugin-help", &shortPluginHelp,
91                    "strip-incl", &stripInclude,
92                    "system-compiler", "Derive the system include paths from this compiler [default /usr/bin/cc]", &systemCompiler,
93                    "td-include", &testDoubleInclude);
94             // dfmt on
95             generateZeroGlobals = !no_zero_globals;
96         } catch (std.getopt.GetOptException ex) {
97             logger.error(ex.msg);
98             help = true;
99         }
100 
101         // default arguments
102         if (stripInclude.length == 0) {
103             stripInclude = r".*/(.*)";
104             logger.trace("--strip-incl: using default regex to strip include path (basename)");
105         }
106 
107         if (config.length != 0) {
108             xmlConfig = readRawConfig(Path(config));
109             if (xmlConfig.isNull) {
110                 invalidXmlConfig = true;
111             }
112         }
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         string system_compiler;
277 
278         Nullable!XmlConfig xmlConfig;
279         CompileCommandFilter compiler_flag_filter;
280         FilterSymbol restrict_symbols;
281         FilterSymbol exclude_symbols;
282 
283         Regex!char[] exclude;
284         Regex!char[] restrict;
285 
286         /// Data produced by the generatore intented to be written to specified file.
287         FileData[] file_data;
288 
289         TestDoubleIncludes td_includes;
290     }
291 
292     static auto makeVariant(ref RawConfiguration args) {
293         // dfmt off
294         auto variant = new CTestDoubleVariant(
295                 MainFileName(args.mainFileName), Path(args.out_),
296                 regex(args.stripInclude))
297             .argPrefix(args.prefix)
298             .argMainName(args.mainName)
299             .argLocationAsComment(args.locationAsComment)
300             .argGmock(args.gmock)
301             .argPreInclude(args.generatePreInclude)
302             .argPostInclude(args.genPostInclude)
303             .argForceTestDoubleIncludes(args.testDoubleInclude)
304             .argFileExclude(args.fileExclude)
305             .argFileRestrict(args.fileRestrict)
306             .argCustomHeader(args.header, args.headerFile)
307             .argGenerateZeroGlobals(args.generateZeroGlobals)
308             .argXmlConfig(args.xmlConfig)
309             .systemCompiler(args.systemCompiler);
310         // dfmt on
311 
312         return variant;
313     }
314 
315     /** Design of c'tor.
316      *
317      * The c'tor has as paramters all the required configuration data.
318      * Assignment of members are used for optional configuration.
319      *
320      * Follows the design pattern "correct by construction".
321      *
322      * TODO document the parameters.
323      */
324     this(MainFileName main_fname, Path output_dir, Regex!char strip_incl) {
325         this.output_dir = output_dir;
326         this.td_includes = TestDoubleIncludes(strip_incl);
327 
328         import std.path : baseName, buildPath, stripExtension;
329 
330         string base_filename = cast(string) main_fname;
331 
332         this.main_file_hdr = Path(buildPath(cast(string) output_dir, base_filename ~ hdrExt));
333         this.main_file_impl = Path(buildPath(cast(string) output_dir, base_filename ~ implExt));
334         this.main_file_globals = Path(buildPath(cast(string) output_dir,
335                 base_filename ~ "_global" ~ implExt));
336         this.gmock_file = Path(buildPath(cast(string) output_dir, base_filename ~ "_gmock" ~ hdrExt));
337         this.pre_incl_file = Path(buildPath(cast(string) output_dir,
338                 base_filename ~ "_pre_includes" ~ hdrExt));
339         this.post_incl_file = Path(buildPath(cast(string) output_dir,
340                 base_filename ~ "_post_includes" ~ hdrExt));
341         this.config_file = Path(buildPath(output_dir, base_filename ~ "_config" ~ xmlExt));
342         this.log_file = Path(buildPath(output_dir, base_filename ~ "_log" ~ xmlExt));
343     }
344 
345     auto argFileExclude(string[] a) {
346         this.exclude = a.map!(a => regex(a)).array();
347         return this;
348     }
349 
350     auto argFileRestrict(string[] a) {
351         this.restrict = a.map!(a => regex(a)).array();
352         return this;
353     }
354 
355     auto argPrefix(string s) {
356         this.prefix = StubPrefix(s);
357         return this;
358     }
359 
360     auto argMainName(string s) {
361         this.main_name = MainName(s);
362         this.main_ns = MainNs(s);
363         this.main_if = MainInterface("I_" ~ s);
364         return this;
365     }
366 
367     /// Force the includes to be those supplied by the user.
368     auto argForceTestDoubleIncludes(string[] a) {
369         if (a.length != 0) {
370             td_includes.forceIncludes(a);
371         }
372         return this;
373     }
374 
375     auto argCustomHeader(string header, string header_file) {
376         if (header.length != 0) {
377             this.custom_hdr = CustomHeader(header);
378         } else if (header_file.length != 0) {
379             import std.file : readText;
380 
381             string content = readText(header_file);
382             this.custom_hdr = CustomHeader(content);
383         }
384 
385         return this;
386     }
387 
388     auto argGmock(bool a) {
389         this.gmock = cast(Flag!"Gmock") a;
390         return this;
391     }
392 
393     auto argPreInclude(bool a) {
394         this.pre_incl = cast(Flag!"PreInclude") a;
395         return this;
396     }
397 
398     auto argPostInclude(bool a) {
399         this.post_incl = cast(Flag!"PostInclude") a;
400         return this;
401     }
402 
403     auto argLocationAsComment(bool a) {
404         this.loc_as_comment = cast(Flag!"locationAsComment") a;
405         return this;
406     }
407 
408     auto argGenerateZeroGlobals(bool value) {
409         this.generate_zero_globals = cast(Flag!"generateZeroGlobals") value;
410         return this;
411     }
412 
413     /** Ensure that the relevant information from the xml file is extracted.
414      *
415      * May overwrite information from the command line.
416      * TODO or should the command line have priority over the xml file?
417      */
418     auto argXmlConfig(Nullable!XmlConfig conf) {
419         import dextool.compilation_db : defaultCompilerFlagFilter;
420 
421         if (conf.isNull) {
422             compiler_flag_filter = CompileCommandFilter(defaultCompilerFlagFilter, 0);
423             return this;
424         }
425 
426         xmlConfig = conf;
427         compiler_flag_filter = CompileCommandFilter(conf.get.filterClangFlags,
428                 conf.get.skipCompilerArgs);
429         restrict_symbols = conf.get.restrictSymbols;
430         exclude_symbols = conf.get.excludeSymbols;
431 
432         return this;
433     }
434 
435     void processIncludes() {
436         td_includes.process();
437     }
438 
439     void finalizeIncludes() {
440         td_includes.finalize();
441     }
442 
443     /// Destination of the configuration file containing how the test double was generated.
444     Path getXmlConfigFile() {
445         return config_file;
446     }
447 
448     /** Destination of the xml log for how dextool was ran when generatinng the
449      * test double.
450      */
451     Path getXmlLog() {
452         return log_file;
453     }
454 
455     ref FilterSymbol getRestrictSymbols() {
456         return restrict_symbols;
457     }
458 
459     ref FilterSymbol getExcludeSymbols() {
460         return exclude_symbols;
461     }
462 
463     ref CompileCommandFilter getCompileCommandFilter() {
464         return compiler_flag_filter;
465     }
466 
467     /// Data produced by the generatore intented to be written to specified file.
468     ref FileData[] getProducedFiles() {
469         return file_data;
470     }
471 
472     void putFile(Path fname, string data) {
473         file_data ~= FileData(fname, data);
474     }
475 
476     auto systemCompiler(string a) {
477         this.system_compiler = a;
478         return this;
479     }
480 
481     // -- Controller --
482 
483     bool doFile(in string filename, in string info) {
484         import dextool.plugin.regex_matchers : matchAny;
485 
486         bool restrict_pass = true;
487         bool exclude_pass = true;
488 
489         if (restrict.length > 0) {
490             restrict_pass = matchAny(filename, restrict);
491             debug {
492                 logger.tracef(!restrict_pass, "--file-restrict skipping %s", info);
493             }
494         }
495 
496         if (exclude.length > 0) {
497             exclude_pass = !matchAny(filename, exclude);
498             debug {
499                 logger.tracef(!exclude_pass, "--file-exclude skipping %s", info);
500             }
501         }
502 
503         return restrict_pass && exclude_pass;
504     }
505 
506     bool doSymbol(string symbol) {
507         // fast path, assuming no symbol filter is the most common
508         if (!restrict_symbols.hasSymbols && !exclude_symbols.hasSymbols) {
509             return true;
510         }
511 
512         if (restrict_symbols.hasSymbols && exclude_symbols.hasSymbols) {
513             return restrict_symbols.contains(symbol) && !exclude_symbols.contains(symbol);
514         }
515 
516         if (restrict_symbols.hasSymbols) {
517             return restrict_symbols.contains(symbol);
518         }
519 
520         if (exclude_symbols.hasSymbols) {
521             return !exclude_symbols.contains(symbol);
522         }
523 
524         return true;
525     }
526 
527     bool doGoogleMock() {
528         return gmock;
529     }
530 
531     bool doPreIncludes() {
532         import std.file : exists;
533 
534         return pre_incl && !exists(cast(string) pre_incl_file);
535     }
536 
537     bool doIncludeOfPreIncludes() {
538         return pre_incl;
539     }
540 
541     bool doPostIncludes() {
542         import std.file : exists;
543 
544         return post_incl && !exists(cast(string) post_incl_file);
545     }
546 
547     bool doIncludeOfPostIncludes() {
548         return post_incl;
549     }
550 
551     bool doLocationAsComment() {
552         return loc_as_comment;
553     }
554 
555     // -- Parameters --
556 
557     Path[] getIncludes() {
558         return td_includes.includes.map!(a => Path(a)).array();
559     }
560 
561     Path 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     Compiler getSystemCompiler() const {
605         return Compiler(system_compiler);
606     }
607 
608     Compiler getMissingFileCompiler() const {
609         if (system_compiler.empty)
610             return Compiler("/usr/bin/cc");
611         return getSystemCompiler();
612     }
613 
614     // -- Products --
615 
616     void putFile(Path fname, CppHModule hdr_data) {
617         file_data ~= FileData(fname, hdr_data.render());
618     }
619 
620     void putFile(Path fname, CppModule impl_data) {
621         file_data ~= FileData(fname, impl_data.render());
622     }
623 
624     void putLocation(Path fname, LocationType type) {
625         td_includes.put(fname, type);
626     }
627 }
628 
629 /// TODO refactor, doing too many things.
630 ExitStatusType genCstub(CTestDoubleVariant variant, string[] userCflags,
631         CompileCommandDB compile_db, Path[] inFiles) {
632     import std.typecons : Yes;
633 
634     import cpptooling.analyzer.clang.context : ClangContext;
635     import dextool.clang : reduceMissingFiles;
636     import dextool.compilation_db : limitOrAllRange, parse, prependFlags,
637         addCompiler, replaceCompiler, addSystemIncludes, fileRange;
638     import dextool.io : writeFileData;
639     import dextool.plugin.ctestdouble.backend.cvariant : CVisitor, Generator;
640     import dextool.utility : prependDefaultFlags, PreferLang, analyzeFile;
641 
642     auto visitor = new CVisitor(variant, variant);
643     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
644     auto generator = Generator(variant, variant, variant);
645 
646     auto compDbRange() {
647         if (compile_db.empty) {
648             return fileRange(inFiles, variant.getMissingFileCompiler);
649         }
650         return compile_db.fileRange;
651     }
652 
653     auto fixedDb = compDbRange.parse(variant.getCompileCommandFilter)
654         .addCompiler(variant.getMissingFileCompiler).replaceCompiler(
655                 variant.getSystemCompiler).addSystemIncludes.prependFlags(
656                 prependDefaultFlags(userCflags, PreferLang.c)).array;
657 
658     auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array)
659         .reduceMissingFiles(fixedDb);
660 
661     if (!compile_db.empty && !limitRange.isMissingFilesEmpty) {
662         foreach (a; limitRange.missingFiles) {
663             logger.error("Unable to find any compiler flags for .", a);
664         }
665         return ExitStatusType.Errors;
666     }
667 
668     foreach (pdata; limitRange.range) {
669         if (analyzeFile(pdata.cmd.absoluteFile, pdata.flags.completeFlags,
670                 visitor, ctx) == ExitStatusType.Errors) {
671             return ExitStatusType.Errors;
672         }
673 
674         generator.aggregate(visitor.root, visitor.container);
675         visitor.clearRoot;
676         variant.processIncludes;
677     }
678 
679     variant.finalizeIncludes;
680 
681     // Analyse and generate test double
682     generator.process(visitor.container);
683 
684     debug {
685         logger.trace(visitor);
686     }
687 
688     return writeFileData(variant.getProducedFiles);
689 }