1 /**
2 Date: 2016-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 backend for analyzing C++ code to generate a test
7 double.
8 
9 Responsible for:
10  - Analyze of the C++ code.
11  - Transform the clang AST to data structures suitable for code generation.
12  - Generate a C++ test double.
13  - Error reporting to the frontend.
14  - Provide an interface to the frontend for such data that a user can control.
15     - What all the test doubles should be prefixed with.
16     - Filename prefix.
17     - To generate a gmock or not.
18 */
19 module dextool.plugin.cpptestdouble.backend.backend;
20 
21 import std.typecons : Flag, Yes;
22 import logger = std.experimental.logger;
23 
24 import dsrcgen.cpp : CppModule, CppHModule;
25 
26 import cpptooling.data : CppNs, CppClassName;
27 import cpptooling.type : CustomHeader, MainInterface, MainNs;
28 
29 import dextool.io : WriteStrategy;
30 
31 import dextool.type : AbsolutePath, DextoolVersion;
32 import cpptooling.data : CppNsStack;
33 import cpptooling.testdouble.header_filter : LocationType;
34 
35 import dextool.plugin.cpptestdouble.backend.generate_cpp : generate;
36 import dextool.plugin.cpptestdouble.backend.interface_ : Controller,
37     Parameters, Products, Transform;
38 import dextool.plugin.cpptestdouble.backend.type : Code, GeneratedData,
39     ImplData, IncludeHooks, Kind;
40 import dextool.plugin.cpptestdouble.backend.visitor : AnalyzeData, CppTUVisitor;
41 
42 /** Backend of test doubles for C++ code.
43  *
44  * Responsible for carrying data between processing steps.
45  *
46  * TODO postProcess shouldn't be a member method.
47  */
48 struct Backend {
49     import std.typecons : Nullable;
50     import libclang_ast.context : ClangContext;
51     import cpptooling.data : CppRoot;
52     import cpptooling.data.symbol : Container;
53     import dextool.type : ExitStatusType;
54 
55     ///
56     this(Controller ctrl, Parameters params, Products products, Transform transform) {
57         this.analyze = AnalyzeData.make;
58         this.ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
59         this.ctrl = ctrl;
60         this.params = params;
61         this.products = products;
62         this.transform = transform;
63     }
64 
65     ExitStatusType analyzeFile(const AbsolutePath abs_in_file, const string[] use_cflags) {
66         import std.typecons : NullableRef, scoped, Nullable;
67         import dextool.utility : analyzeFile;
68         import cpptooling.data : MergeMode;
69 
70         NullableRef!Container cont_ = &container;
71         NullableRef!AnalyzeData analyz = &analyze.get();
72         auto visitor = new CppTUVisitor(ctrl, products, analyz, cont_);
73 
74         if (analyzeFile(abs_in_file, use_cflags, visitor, ctx) == ExitStatusType.Errors) {
75             return ExitStatusType.Errors;
76         }
77 
78         debug logger.tracef("%u", visitor.root);
79 
80         auto fl = rawFilter(visitor.root, ctrl, products,
81                 (Nullable!USRType usr) => container.find!LocationTag(usr.get),
82                 (USRType usr) => container.find!LocationTag(usr));
83 
84         analyze.get.root.merge(fl, MergeMode.full);
85 
86         return ExitStatusType.Ok;
87     }
88 
89     /** Process structural data to a test double.
90      *
91      * raw -> filter -> translate -> code gen.
92      *
93      * Filters the structural data.
94      * Controller is involved to allow filtering of identifiers according to
95      * there lexical location.
96      *
97      * Translate prepares the data for code generator.
98      * Extra structures needed for code generation is made at this stage.
99      * On demand extra data is created. An example of on demand is --gmock.
100      *
101      * Code generation is a straight up translation.
102      * Logical decisions should have been handled in earlier stages.
103      */
104     void process() {
105         import cpptooling.data.symbol.types : USRType;
106 
107         assert(!analyze.isNull);
108 
109         debug logger.trace(container.toString);
110 
111         logger.tracef("Filtered:\n%u", analyze.get.root);
112 
113         auto impl_data = ImplData.make();
114         impl_data.includeHooks = IncludeHooks.make(transform);
115 
116         impl_data.putForLookup(analyze.get.classes);
117         translate(analyze.get.root, container, ctrl, params, impl_data);
118         analyze.nullify();
119 
120         logger.tracef("Translated to implementation:\n%u", impl_data.root);
121         logger.trace("kind:\n", impl_data.kind);
122 
123         GeneratedData gen_data;
124         gen_data.includeHooks = impl_data.includeHooks;
125         generate(impl_data, ctrl, params, gen_data, container);
126         postProcess(ctrl, params, products, transform, gen_data);
127     }
128 
129 private:
130     ClangContext ctx;
131     Controller ctrl;
132     Container container;
133     Nullable!AnalyzeData analyze;
134     Parameters params;
135     Products products;
136     Transform transform;
137 }
138 
139 private:
140 
141 @safe:
142 
143 import cpptooling.data : CppRoot, CppClass, CppMethod, CppCtor, CppDtor,
144     CFunction, CppNamespace, LocationTag, Location;
145 import cpptooling.data.symbol : Container, USRType;
146 import dsrcgen.cpp : E;
147 
148 /** Filter the raw IR according to the users desire.
149  *
150  * TODO should handle StorageClass like cvariant do.
151  *
152  * Ignoring globals by ignoring the root ranges globalRange.
153  *
154  * Params:
155  *  ctrl = removes according to directives via ctrl
156  */
157 CppT rawFilter(CppT, LookupT, LookupT2)(CppT input, Controller ctrl,
158         Products prod, LookupT lookup, LookupT2 lookup2) @safe {
159     import std.array : array;
160     import std.algorithm : each, filter, map, filter;
161     import std.range : tee;
162     import dextool.type : Path;
163     import cpptooling.data : StorageClass;
164     import cpptooling.generator.utility : filterAnyLocation;
165 
166     // setup
167     static if (is(CppT == CppRoot)) {
168         auto filtered = CppRoot.make;
169     } else static if (is(CppT == CppNamespace)) {
170         auto filtered = CppNamespace(input.resideInNs);
171         assert(!input.isAnonymous);
172         assert(input.name.length > 0);
173     } else {
174         static assert("Type not supported: " ~ CppT.stringof);
175     }
176 
177     if (ctrl.doFreeFunction) {
178         // dfmt off
179         input.funcRange
180             .filter!(a => !a.usr.isNull)
181             // by definition static functions can't be replaced by test doubles
182             .filter!(a => a.storageClass != StorageClass.Static)
183             // ask controller if the file should be mocked, and thus the node
184             .filterAnyLocation!(a => ctrl.doFile(a.location.file, cast(string) a.value.name ~ " " ~ a.location.toString))(lookup)
185             // pass on location as a product to be used to calculate #include
186             .tee!(a => prod.putLocation(Path(a.location.file), LocationType.Leaf))
187             .each!(a => filtered.put(a.value));
188         // dfmt on
189     }
190 
191     // dfmt off
192     input.namespaceRange
193         .filter!(a => !a.isAnonymous)
194         .map!(a => rawFilter(a, ctrl, prod, lookup, lookup2))
195         .each!(a => filtered.put(a));
196     // dfmt on
197 
198     foreach (a; input.classRange // ask controller (the user) if the file should be mocked
199         .filterAnyLocation!(a => ctrl.doFile(a.location.file,
200             cast(string) a.value.name ~ " " ~ a.location.toString))(lookup)) {
201 
202         if (ctrl.doGoogleMock && a.value.isVirtual) {
203         } else if (ctrl.doGoogleTestPODPrettyPrint && a.value.memberPublicRange.length != 0) {
204         } else {
205             // skip the class
206             continue;
207         }
208 
209         filtered.put(a.value);
210         prod.putLocation(Path(a.location.file), LocationType.Leaf);
211     }
212 
213     return filtered;
214 }
215 
216 /** Structurally transform the input to a test double implementation.
217  *
218  * In other words it the input IR (that has been filtered) is transformed to an
219  * IR representing what code to generate.
220  */
221 void translate(CppRoot root, ref Container container, Controller ctrl,
222         Parameters params, ref ImplData impl) {
223     import std.algorithm : map, filter, each;
224     import cpptooling.data : mergeClassInherit, FullyQualifiedNameType;
225 
226     if (!root.funcRange.empty) {
227         translateToTestDoubleForFreeFunctions(root, impl, cast(Flag!"doGoogleMock") ctrl.doGoogleMock,
228                 CppNsStack.init, params.getMainNs, params.getMainInterface, impl.root);
229     }
230 
231     foreach (a; root.namespaceRange
232             .map!(a => translate(a, impl, container, ctrl, params))
233             .filter!(a => !a.empty)) {
234         impl.root.put(a);
235     }
236 
237     foreach (a; root.classRange.map!(a => mergeClassInherit(a, container, a => impl.lookupClass(a)))) {
238         // check it is virtual.
239         // can happen that the result is a class with no methods, thus in state Unknown
240         if (ctrl.doGoogleMock && a.isVirtual) {
241             import cpptooling.generator.gmock : makeGmock;
242 
243             auto mock = makeGmock(a);
244             impl.tag(mock.id, Kind.gmock);
245             impl.root.put(mock);
246         }
247 
248         if (ctrl.doGoogleTestPODPrettyPrint && a.memberPublicRange.length != 0) {
249             impl.tag(a.id, Kind.gtestPrettyPrint);
250             impl.root.put(a);
251         }
252     }
253 }
254 
255 /** Translate namspaces and the content to test double implementations.
256  */
257 CppNamespace translate(CppNamespace input, ref ImplData data,
258         ref Container container, Controller ctrl, Parameters params) {
259     import std.algorithm : map, filter, each;
260     import std.array : empty;
261     import cpptooling.data : CppNsStack, CppNs, mergeClassInherit, FullyQualifiedNameType;
262 
263     static auto makeGmockInNs(CppClass c, CppNsStack ns_hier, ref ImplData data) {
264         import cpptooling.data : CppNs;
265         import cpptooling.generator.gmock : makeGmock;
266 
267         auto ns = CppNamespace(ns_hier);
268         data.tag(ns.id, Kind.testDoubleNamespace);
269         auto mock = makeGmock(c);
270         data.tag(mock.id, Kind.gmock);
271         ns.put(mock);
272         return ns;
273     }
274 
275     auto ns = CppNamespace(input.resideInNs);
276 
277     if (!input.funcRange.empty) {
278         translateToTestDoubleForFreeFunctions(input, data, cast(Flag!"doGoogleMock") ctrl.doGoogleMock,
279                 ns.resideInNs, params.getMainNs, params.getMainInterface, ns);
280     }
281 
282     input.namespaceRange().map!(a => translate(a, data, container, ctrl, params))
283         .filter!(a => !a.empty)
284         .each!(a => ns.put(a));
285 
286     foreach (class_; input.classRange.map!(a => mergeClassInherit(a, container,
287             a => data.lookupClass(a)))) {
288         // check it is virtual.
289         // can happen that the result is a class with no methods, thus in state Unknown
290         if (ctrl.doGoogleMock && class_.isVirtual) {
291             auto mock = makeGmockInNs(class_, CppNsStack(ns.resideInNs.dup,
292                     CppNs(params.getMainNs)), data);
293             ns.put(mock);
294         }
295 
296         if (ctrl.doGoogleTestPODPrettyPrint && class_.memberPublicRange.length != 0) {
297             data.tag(class_.id, Kind.gtestPrettyPrint);
298             ns.put(class_);
299         }
300     }
301 
302     return ns;
303 }
304 
305 void translateToTestDoubleForFreeFunctions(InT, OutT)(ref InT input, ref ImplData data,
306         Flag!"doGoogleMock" do_gmock, CppNsStack reside_in_ns, MainNs main_ns,
307         MainInterface main_if, ref OutT ns) {
308     import std.algorithm : each;
309     import dextool.plugin.backend.cpptestdouble.adapter : makeAdapter, makeSingleton;
310     import cpptooling.data : CppNs, CppClassName;
311     import cpptooling.generator.func : makeFuncInterface;
312     import cpptooling.generator.gmock : makeGmock;
313 
314     // singleton instance must be before the functions
315     auto singleton = makeSingleton(main_ns, main_if);
316     data.tag(singleton.id, Kind.testDoubleSingleton);
317     ns.put(singleton);
318 
319     // output the functions using the singleton
320     input.funcRange.each!(a => ns.put(a));
321 
322     auto td_ns = CppNamespace(CppNsStack(reside_in_ns.dup, CppNs(main_ns)));
323     data.tag(td_ns.id, Kind.testDoubleNamespace);
324 
325     auto i_free_func = makeFuncInterface(input.funcRange, CppClassName(main_if), td_ns.resideInNs);
326     data.tag(i_free_func.id, Kind.testDoubleInterface);
327     td_ns.put(i_free_func);
328 
329     auto adapter = makeAdapter(main_if).makeTestDouble(true).finalize;
330     data.tag(adapter.id, Kind.adapter);
331     td_ns.put(adapter);
332 
333     if (do_gmock) {
334         auto mock = makeGmock(i_free_func);
335         data.tag(mock.id, Kind.gmock);
336         td_ns.put(mock);
337     }
338 
339     ns.put(td_ns);
340 }
341 
342 void postProcess(Controller ctrl, Parameters params, Products prods,
343         Transform transf, ref GeneratedData gen_data) {
344     import std.path : baseName;
345     import cpptooling.generator.includes : convToIncludeGuard,
346         generatePreInclude, generatePostInclude, makeHeader;
347 
348     //TODO copied code from cstub. consider separating from this module to
349     // allow reuse.
350     static auto outputHdr(CppModule hdr, AbsolutePath fname, DextoolVersion ver,
351             CustomHeader custom_hdr) {
352         auto o = CppHModule(convToIncludeGuard(fname));
353         o.header.append(makeHeader(fname, ver, custom_hdr));
354         o.content.append(hdr);
355 
356         return o;
357     }
358 
359     static auto output(CppModule code, AbsolutePath incl_fname, AbsolutePath dest,
360             DextoolVersion ver, CustomHeader custom_hdr) {
361         import std.path : baseName;
362 
363         auto o = new CppModule;
364         o.suppressIndent(1);
365         o.append(makeHeader(dest, ver, custom_hdr));
366         o.include(incl_fname.baseName);
367         o.sep(2);
368         o.append(code);
369 
370         return o;
371     }
372 
373     auto test_double_hdr = transf.createHeaderFile(null);
374 
375     foreach (k, v; gen_data.uniqueData) {
376         final switch (k) with (Code) {
377         case Kind.hdr:
378             prods.putFile(test_double_hdr, outputHdr(v,
379                     test_double_hdr, params.getToolVersion, params.getCustomHeader));
380             break;
381         case Kind.impl:
382             auto test_double_cpp = transf.createImplFile(null);
383             prods.putFile(test_double_cpp, output(v, test_double_hdr,
384                     test_double_cpp, params.getToolVersion, params.getCustomHeader));
385             break;
386         }
387     }
388 
389     auto mock_incls = new CppModule;
390     foreach (mock; gen_data.gmocks) {
391         import std.algorithm : joiner, map;
392         import std.conv : text;
393         import std.format : format;
394         import std..string : toLower;
395         import cpptooling.generator.gmock : generateGmockHdr;
396 
397         string repr_ns = mock.nesting.map!(a => a.toLower).joiner("-").text;
398         string ns_suffix = mock.nesting.length != 0 ? "-" : "";
399         auto fname = transf.createHeaderFile(format("_%s%s%s_gmock", repr_ns,
400                 ns_suffix, mock.name.toLower));
401 
402         mock_incls.include(fname.baseName);
403 
404         prods.putFile(fname, generateGmockHdr(test_double_hdr, fname,
405                 params.getToolVersion, params.getCustomHeader, mock));
406     }
407 
408     //TODO code duplication, merge with the above
409     foreach (gtest; gen_data.gtestPPHdr) {
410         import cpptooling.generator.gtest : generateGtestHdr;
411 
412         auto fname = transf.createHeaderFile(makeGtestFileName(transf, gtest.nesting, gtest.name));
413         mock_incls.include(fname.baseName);
414 
415         prods.putFile(fname, generateGtestHdr(test_double_hdr, fname,
416                 params.getToolVersion, params.getCustomHeader, gtest));
417     }
418 
419     auto gtest_impl = new CppModule;
420     gtest_impl.comment("Compile this file to automatically compile all generated pretty printer");
421     //TODO code duplication, merge with the above
422     foreach (gtest; gen_data.gtestPPImpl) {
423         auto fname_hdr = transf.createHeaderFile(makeGtestFileName(transf,
424                 gtest.nesting, gtest.name));
425         auto fname_cpp = transf.createImplFile(makeGtestFileName(transf,
426                 gtest.nesting, gtest.name));
427         gtest_impl.include(fname_cpp.baseName);
428         prods.putFile(fname_cpp, output(gtest, fname_hdr, fname_cpp,
429                 params.getToolVersion, params.getCustomHeader));
430     }
431 
432     const f_gmock_hdr = transf.createHeaderFile("_gmock");
433     if (gen_data.gmocks.length != 0 || gen_data.gtestPPHdr.length != 0) {
434         prods.putFile(f_gmock_hdr, outputHdr(mock_incls, f_gmock_hdr,
435                 params.getToolVersion, params.getCustomHeader));
436     }
437 
438     if (gen_data.gtestPPHdr.length != 0) {
439         auto fname = transf.createImplFile("_fused_gtest");
440         prods.putFile(fname, output(gtest_impl, f_gmock_hdr, fname,
441                 params.getToolVersion, params.getCustomHeader));
442     }
443 
444     if (ctrl.doPreIncludes) {
445         prods.putFile(gen_data.includeHooks.preInclude,
446                 generatePreInclude(gen_data.includeHooks.preInclude), WriteStrategy.skip);
447     }
448 
449     if (ctrl.doPostIncludes) {
450         prods.putFile(gen_data.includeHooks.postInclude,
451                 generatePostInclude(gen_data.includeHooks.postInclude), WriteStrategy.skip);
452     }
453 }
454 
455 string makeGtestFileName(Transform transf, CppNs[] nesting, CppClassName name) {
456     import std.algorithm : joiner, map;
457     import std.conv : text;
458     import std.format : format;
459     import std..string : toLower;
460 
461     string repr_ns = nesting.map!(a => a.toLower).joiner("-").text;
462     string ns_suffix = nesting.length != 0 ? "-" : "";
463     return format("_%s%s%s_gtest", repr_ns, ns_suffix, name.toLower);
464 }