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