1 /**
2 Date: 2015-2017, Joakim Brännström
3 License: MPL-2, Mozilla Public License 2.0
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This file contains the frontend for generating a C++ test double.
7 
8 Responsible for:
9  - Receiving the call from the main to start working.
10  - User interaction.
11     - Error reporting in a way that the user understand the error.
12     - Writing files to the filesystem.
13     - Parsing arguments and other interaction information from the user.
14     - Configuration file handling.
15  - Provide user data to the backend via the interface the backend own.
16 */
17 module dextool.plugin.cpptestdouble.frontend.frontend;
18 
19 import std.algorithm : map;
20 import std.array : array, empty;
21 import std.typecons : Nullable, Yes;
22 
23 import logger = std.experimental.logger;
24 
25 import cpptooling.type : CustomHeader, MainFileName, MainName, MainNs;
26 
27 import dextool.compilation_db : CompileCommandDB, Compiler;
28 import dextool.type : AbsolutePath, DextoolVersion, ExitStatusType, Path;
29 import dextool.io : WriteStrategy;
30 
31 import dextool.plugin.cpptestdouble.backend : Controller, Parameters, Products, Transform;
32 import dextool.plugin.cpptestdouble.frontend.raw_args : Config_YesNo, RawConfiguration, XmlConfig;
33 
34 struct FileData {
35     AbsolutePath filename;
36     string data;
37     WriteStrategy strategy;
38 }
39 
40 /** Test double generation of C++ code.
41  *
42  * TODO Describe the options.
43  * TODO implement --in=...
44  */
45 class CppTestDoubleVariant : Controller, Parameters, Products {
46     import std.string : toLower;
47     import std.regex : regex, Regex;
48     import std.typecons : Flag;
49     import dextool.compilation_db : CompileCommandFilter;
50     import cpptooling.type : StubPrefix, MainInterface;
51     import dextool.utility;
52     import cpptooling.testdouble.header_filter : TestDoubleIncludes, LocationType;
53     import dsrcgen.cpp;
54 
55     private {
56         StubPrefix prefix;
57 
58         CustomHeader custom_hdr;
59 
60         MainName main_name;
61         MainNs main_ns;
62         MainInterface main_if;
63         Flag!"FreeFunction" do_free_funcs;
64         Flag!"Gmock" gmock;
65         Flag!"GtestPODPrettyPrint" gtestPP;
66         Flag!"PreInclude" pre_incl;
67         Flag!"PostInclude" post_incl;
68 
69         string system_compiler;
70 
71         Nullable!XmlConfig xmlConfig;
72         CompileCommandFilter compiler_flag_filter;
73 
74         Regex!char[] exclude;
75         Regex!char[] restrict;
76 
77         /// Data produced by the generatore intented to be written to specified file.
78         FileData[] file_data;
79 
80         TestDoubleIncludes td_includes;
81     }
82 
83     static auto makeVariant(ref RawConfiguration args) {
84         // dfmt off
85         auto variant = new CppTestDoubleVariant(regex(args.stripInclude))
86             .argPrefix(args.prefix)
87             .argMainName(args.mainName)
88             .argGenFreeFunction(args.doFreeFuncs)
89             .argGmock(args.gmock)
90             .argGtestPODPrettyPrint(args.gtestPODPrettyPrint)
91             .argPreInclude(args.generatePreInclude)
92             .argPostInclude(args.genPostInclude)
93             .argForceTestDoubleIncludes(args.testDoubleInclude)
94             .argFileExclude(args.fileExclude)
95             .argFileRestrict(args.fileRestrict)
96             .argCustomHeader(args.header, args.headerFile)
97             .argXmlConfig(args.xmlConfig)
98             .systemCompiler(args.systemCompiler);
99         // dfmt on
100 
101         return variant;
102     }
103 
104     /** Design of c'tor.
105      *
106      * The c'tor has as paramters all the required configuration data.
107      * Assignment of members are used for optional configuration.
108      *
109      * Follows the design pattern "correct by construction".
110      *
111      * TODO document the parameters.
112      */
113     this(Regex!char strip_incl) {
114         this.td_includes = TestDoubleIncludes(strip_incl);
115     }
116 
117     auto argFileExclude(string[] a) {
118         this.exclude = a.map!(a => regex(a)).array();
119         return this;
120     }
121 
122     auto argFileRestrict(string[] a) {
123         this.restrict = a.map!(a => regex(a)).array();
124         return this;
125     }
126 
127     auto argPrefix(string s) {
128         this.prefix = StubPrefix(s);
129         return this;
130     }
131 
132     auto argMainName(string s) {
133         this.main_name = MainName(s);
134         this.main_ns = MainNs(s);
135         this.main_if = MainInterface("I_" ~ s);
136         return this;
137     }
138 
139     /// Force the includes to be those supplied by the user.
140     auto argForceTestDoubleIncludes(string[] a) {
141         if (a.length != 0) {
142             td_includes.forceIncludes(a);
143         }
144         return this;
145     }
146 
147     auto argCustomHeader(string header, string header_file) {
148         if (header.length != 0) {
149             this.custom_hdr = CustomHeader(header);
150         } else if (header_file.length != 0) {
151             import std.file : readText;
152 
153             string content = readText(header_file);
154             this.custom_hdr = CustomHeader(content);
155         }
156 
157         return this;
158     }
159 
160     auto argGenFreeFunction(bool a) {
161         this.do_free_funcs = cast(Flag!"FreeFunction") a;
162         return this;
163     }
164 
165     auto argGmock(bool a) {
166         this.gmock = cast(Flag!"Gmock") a;
167         return this;
168     }
169 
170     auto argGtestPODPrettyPrint(Config_YesNo a) {
171         this.gtestPP = cast(Flag!"GtestPODPrettyPrint")(cast(bool) a);
172         return this;
173     }
174 
175     auto argPreInclude(bool a) {
176         this.pre_incl = cast(Flag!"PreInclude") a;
177         return this;
178     }
179 
180     auto argPostInclude(bool a) {
181         this.post_incl = cast(Flag!"PostInclude") a;
182         return this;
183     }
184 
185     /** Ensure that the relevant information from the xml file is extracted.
186      *
187      * May overwrite information from the command line.
188      * TODO or should the command line have priority over the xml file?
189      */
190     auto argXmlConfig(Nullable!XmlConfig conf) {
191         import dextool.compilation_db : defaultCompilerFlagFilter;
192 
193         if (conf.isNull) {
194             compiler_flag_filter = CompileCommandFilter(defaultCompilerFlagFilter, 0);
195             return this;
196         }
197 
198         xmlConfig = conf;
199         compiler_flag_filter = CompileCommandFilter(conf.get.filterClangFlags,
200                 conf.get.skipCompilerArgs);
201 
202         return this;
203     }
204 
205     auto systemCompiler(string a) {
206         this.system_compiler = a;
207         return this;
208     }
209 
210     ref CompileCommandFilter getCompileCommandFilter() {
211         return compiler_flag_filter;
212     }
213 
214     /// Data produced by the generatore intented to be written to specified file.
215     ref FileData[] getProducedFiles() {
216         return file_data;
217     }
218 
219     void putFile(AbsolutePath fname, string data) {
220         file_data ~= FileData(fname, data);
221     }
222 
223     /// Signal that a file has finished analyzing.
224     void processIncludes() {
225         td_includes.process();
226     }
227 
228     /// Signal that all files have been analyzed.
229     void finalizeIncludes() {
230         td_includes.finalize();
231     }
232 
233     // -- Controller --
234 
235     bool doFile(in string filename, in string info) {
236         import dextool.plugin.regex_matchers : matchAny;
237 
238         bool restrict_pass = true;
239         bool exclude_pass = true;
240 
241         if (restrict.length > 0) {
242             restrict_pass = matchAny(filename, restrict);
243             debug {
244                 logger.tracef(!restrict_pass, "--file-restrict skipping %s", info);
245             }
246         }
247 
248         if (exclude.length > 0) {
249             exclude_pass = !matchAny(filename, exclude);
250             debug {
251                 logger.tracef(!exclude_pass, "--file-exclude skipping %s", info);
252             }
253         }
254 
255         return restrict_pass && exclude_pass;
256     }
257 
258     bool doGoogleMock() {
259         return gmock;
260     }
261 
262     bool doGoogleTestPODPrettyPrint() {
263         return gtestPP;
264     }
265 
266     bool doPreIncludes() {
267         return pre_incl;
268     }
269 
270     bool doIncludeOfPreIncludes() {
271         return pre_incl;
272     }
273 
274     bool doPostIncludes() {
275         return post_incl;
276     }
277 
278     bool doIncludeOfPostIncludes() {
279         return post_incl;
280     }
281 
282     bool doFreeFunction() {
283         return do_free_funcs;
284     }
285 
286     // -- Parameters --
287 
288     Path[] getIncludes() {
289         return td_includes.includes.map!(a => Path(a)).array();
290     }
291 
292     MainName getMainName() {
293         return main_name;
294     }
295 
296     MainNs getMainNs() {
297         return main_ns;
298     }
299 
300     MainInterface getMainInterface() {
301         return main_if;
302     }
303 
304     StubPrefix getArtifactPrefix() {
305         return prefix;
306     }
307 
308     DextoolVersion getToolVersion() {
309         import dextool.utility : dextoolVersion;
310 
311         return dextoolVersion;
312     }
313 
314     CustomHeader getCustomHeader() {
315         return custom_hdr;
316     }
317 
318     Compiler getSystemCompiler() const {
319         return Compiler(system_compiler);
320     }
321 
322     Compiler getMissingFileCompiler() const {
323         if (system_compiler.empty)
324             return Compiler("/usr/bin/c++");
325         return getSystemCompiler();
326     }
327 
328     // -- Products --
329 
330     void putFile(AbsolutePath fname, CppHModule hdr_data) {
331         file_data ~= FileData(fname, hdr_data.render());
332     }
333 
334     void putFile(AbsolutePath fname, CppHModule data, WriteStrategy strategy) {
335         file_data ~= FileData(fname, data.render(), strategy);
336     }
337 
338     void putFile(AbsolutePath fname, CppModule impl_data) {
339         file_data ~= FileData(fname, impl_data.render());
340     }
341 
342     void putLocation(Path fname, LocationType type) {
343         td_includes.put(fname, type);
344     }
345 }
346 
347 class FrontendTransform : Transform {
348     import std.path : buildPath;
349     import cpptooling.type : StubPrefix;
350 
351     static const hdrExt = ".hpp";
352     static const implExt = ".cpp";
353     static const xmlExt = ".xml";
354 
355     StubPrefix prefix;
356 
357     Path output_dir;
358     MainFileName main_fname;
359 
360     this(MainFileName main_fname, Path output_dir) {
361         this.main_fname = main_fname;
362         this.output_dir = output_dir;
363     }
364 
365     AbsolutePath createHeaderFile(string name) {
366         return AbsolutePath(Path(buildPath(output_dir, main_fname ~ name ~ hdrExt)));
367     }
368 
369     AbsolutePath createImplFile(string name) {
370         return AbsolutePath(Path(buildPath(output_dir, main_fname ~ name ~ implExt)));
371     }
372 
373     AbsolutePath createXmlFile(string name) {
374         return AbsolutePath(Path(buildPath(output_dir, main_fname ~ name ~ xmlExt)));
375     }
376 }
377 
378 ExitStatusType genCpp(CppTestDoubleVariant variant, FrontendTransform transform,
379         string[] userCflags, CompileCommandDB compile_db, Path[] inFiles) {
380     import dextool.clang : reduceMissingFiles;
381     import dextool.compilation_db : limitOrAllRange, parse, prependFlags,
382         addCompiler, replaceCompiler, addSystemIncludes, fileRange;
383     import dextool.plugin.cpptestdouble.backend : Backend;
384     import dextool.io : writeFileData;
385     import dextool.utility : prependDefaultFlags, PreferLang;
386 
387     auto generator = Backend(variant, variant, variant, transform);
388 
389     auto compDbRange() {
390         if (compile_db.empty) {
391             return fileRange(inFiles, variant.getMissingFileCompiler);
392         }
393         return compile_db.fileRange;
394     }
395 
396     auto fixedDb = compDbRange.parse(variant.getCompileCommandFilter)
397         .addCompiler(variant.getMissingFileCompiler).replaceCompiler(
398                 variant.getSystemCompiler).addSystemIncludes.prependFlags(
399                 prependDefaultFlags(userCflags, PreferLang.cpp)).array;
400 
401     auto limitRange = limitOrAllRange(fixedDb, inFiles.map!(a => cast(string) a).array)
402         .reduceMissingFiles(fixedDb);
403 
404     if (!compile_db.empty && !limitRange.isMissingFilesEmpty) {
405         foreach (a; limitRange.missingFiles) {
406             logger.error("Unable to find any compiler flags for .", a);
407         }
408         return ExitStatusType.Errors;
409     }
410 
411     foreach (pdata; limitRange.range) {
412         if (generator.analyzeFile(pdata.cmd.absoluteFile,
413                 pdata.flags.completeFlags) == ExitStatusType.Errors) {
414             return ExitStatusType.Errors;
415         }
416 
417         variant.processIncludes;
418     }
419 
420     variant.finalizeIncludes;
421 
422     // All files analyzed, process and generate artifacts.
423     generator.process();
424 
425     return writeFileData(variant.file_data);
426 }