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