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;
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                 (USRType usr) => container.find!LocationTag(usr));
82 
83         analyze.get.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         assert(!analyze.isNull);
107 
108         debug logger.trace(container.toString);
109 
110         logger.tracef("Filtered:\n%u", analyze.get.root);
111 
112         auto impl_data = ImplData.make();
113         impl_data.includeHooks = IncludeHooks.make(transform);
114 
115         impl_data.putForLookup(analyze.get.classes);
116         translate(analyze.get.root, container, ctrl, params, impl_data);
117         analyze.nullify();
118 
119         logger.tracef("Translated to implementation:\n%u", impl_data.root);
120         logger.trace("kind:\n", impl_data.kind);
121 
122         GeneratedData gen_data;
123         gen_data.includeHooks = impl_data.includeHooks;
124         generate(impl_data, ctrl, params, gen_data, container);
125         postProcess(ctrl, params, products, transform, gen_data);
126     }
127 
128 private:
129     ClangContext ctx;
130     Controller ctrl;
131     Container container;
132     Nullable!AnalyzeData analyze;
133     Parameters params;
134     Products products;
135     Transform transform;
136 }
137 
138 private:
139 
140 @safe:
141 
142 import cpptooling.data : CppRoot, CppClass, CppMethod, CppCtor, CppDtor,
143     CFunction, CppNamespace, LocationTag, Location;
144 import cpptooling.data.symbol : Container, USRType;
145 import dsrcgen.cpp : E;
146 
147 /** Filter the raw IR according to the users desire.
148  *
149  * TODO should handle StorageClass like cvariant do.
150  *
151  * Ignoring globals by ignoring the root ranges globalRange.
152  *
153  * Params:
154  *  ctrl = removes according to directives via ctrl
155  */
156 CppT rawFilter(CppT, LookupT)(CppT input, Controller ctrl, Products prod, LookupT lookup) @safe {
157     import std.array : array;
158     import std.algorithm : each, filter, map, filter;
159     import std.range : tee;
160     import dextool.type : Path;
161     import cpptooling.data : StorageClass;
162     import cpptooling.generator.utility : filterAnyLocation;
163 
164     // setup
165     static if (is(CppT == CppRoot)) {
166         auto filtered = CppRoot.make;
167     } else static if (is(CppT == CppNamespace)) {
168         auto filtered = CppNamespace(input.resideInNs);
169         assert(!input.isAnonymous);
170         assert(input.name.length > 0);
171     } else {
172         static assert("Type not supported: " ~ CppT.stringof);
173     }
174 
175     if (ctrl.doFreeFunction) {
176         // dfmt off
177         input.funcRange
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))
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 cpptooling.data : mergeClassInherit, FullyQualifiedNameType;
222 
223     if (!root.funcRange.empty) {
224         translateToTestDoubleForFreeFunctions(root, impl, cast(Flag!"doGoogleMock") ctrl.doGoogleMock,
225                 CppNsStack.init, params.getMainNs, params.getMainInterface, impl.root);
226     }
227 
228     foreach (a; root.namespaceRange
229             .map!(a => translate(a, impl, container, ctrl, params))
230             .filter!(a => !a.empty)) {
231         impl.root.put(a);
232     }
233 
234     foreach (a; root.classRange.map!(a => mergeClassInherit(a, container, a => impl.lookupClass(a)))) {
235         // check it is virtual.
236         // can happen that the result is a class with no methods, thus in state Unknown
237         if (ctrl.doGoogleMock && a.isVirtual) {
238             import cpptooling.generator.gmock : makeGmock;
239 
240             auto mock = makeGmock(a);
241             impl.tag(mock.id, Kind.gmock);
242             impl.root.put(mock);
243         }
244 
245         if (ctrl.doGoogleTestPODPrettyPrint && a.memberPublicRange.length != 0) {
246             impl.tag(a.id, Kind.gtestPrettyPrint);
247             impl.root.put(a);
248         }
249     }
250 }
251 
252 /** Translate namspaces and the content to test double implementations.
253  */
254 CppNamespace translate(CppNamespace input, ref ImplData data,
255         ref Container container, Controller ctrl, Parameters params) {
256     import std.algorithm : map, filter, each;
257     import std.array : empty;
258     import cpptooling.data : CppNsStack, CppNs, mergeClassInherit, FullyQualifiedNameType;
259 
260     static auto makeGmockInNs(CppClass c, CppNsStack ns_hier, ref ImplData data) {
261         import cpptooling.data : CppNs;
262         import cpptooling.generator.gmock : makeGmock;
263 
264         auto ns = CppNamespace(ns_hier);
265         data.tag(ns.id, Kind.testDoubleNamespace);
266         auto mock = makeGmock(c);
267         data.tag(mock.id, Kind.gmock);
268         ns.put(mock);
269         return ns;
270     }
271 
272     auto ns = CppNamespace(input.resideInNs);
273 
274     if (!input.funcRange.empty) {
275         translateToTestDoubleForFreeFunctions(input, data, cast(Flag!"doGoogleMock") ctrl.doGoogleMock,
276                 ns.resideInNs, params.getMainNs, params.getMainInterface, ns);
277     }
278 
279     input.namespaceRange().map!(a => translate(a, data, container, ctrl, params))
280         .filter!(a => !a.empty)
281         .each!(a => ns.put(a));
282 
283     foreach (class_; input.classRange.map!(a => mergeClassInherit(a, container,
284             a => data.lookupClass(a)))) {
285         // check it is virtual.
286         // can happen that the result is a class with no methods, thus in state Unknown
287         if (ctrl.doGoogleMock && class_.isVirtual) {
288             auto mock = makeGmockInNs(class_, CppNsStack(ns.resideInNs.dup,
289                     CppNs(params.getMainNs)), data);
290             ns.put(mock);
291         }
292 
293         if (ctrl.doGoogleTestPODPrettyPrint && class_.memberPublicRange.length != 0) {
294             data.tag(class_.id, Kind.gtestPrettyPrint);
295             ns.put(class_);
296         }
297     }
298 
299     return ns;
300 }
301 
302 void translateToTestDoubleForFreeFunctions(InT, OutT)(ref InT input, ref ImplData data,
303         Flag!"doGoogleMock" do_gmock, CppNsStack reside_in_ns, MainNs main_ns,
304         MainInterface main_if, ref OutT ns) {
305     import std.algorithm : each;
306     import dextool.plugin.backend.cpptestdouble.adapter : makeAdapter, makeSingleton;
307     import cpptooling.data : CppNs, CppClassName;
308     import cpptooling.generator.func : makeFuncInterface;
309     import cpptooling.generator.gmock : makeGmock;
310 
311     // singleton instance must be before the functions
312     auto singleton = makeSingleton(main_ns, main_if);
313     data.tag(singleton.id, Kind.testDoubleSingleton);
314     ns.put(singleton);
315 
316     // output the functions using the singleton
317     input.funcRange.each!(a => ns.put(a));
318 
319     auto td_ns = CppNamespace(CppNsStack(reside_in_ns.dup, CppNs(main_ns)));
320     data.tag(td_ns.id, Kind.testDoubleNamespace);
321 
322     auto i_free_func = makeFuncInterface(input.funcRange, CppClassName(main_if), td_ns.resideInNs);
323     data.tag(i_free_func.id, Kind.testDoubleInterface);
324     td_ns.put(i_free_func);
325 
326     auto adapter = makeAdapter(main_if).makeTestDouble(true).finalize;
327     data.tag(adapter.id, Kind.adapter);
328     td_ns.put(adapter);
329 
330     if (do_gmock) {
331         auto mock = makeGmock(i_free_func);
332         data.tag(mock.id, Kind.gmock);
333         td_ns.put(mock);
334     }
335 
336     ns.put(td_ns);
337 }
338 
339 void postProcess(Controller ctrl, Parameters params, Products prods,
340         Transform transf, ref GeneratedData gen_data) {
341     import std.path : baseName;
342     import cpptooling.generator.includes : convToIncludeGuard,
343         generatePreInclude, generatePostInclude, makeHeader;
344 
345     //TODO copied code from cstub. consider separating from this module to
346     // allow reuse.
347     static auto outputHdr(CppModule hdr, AbsolutePath fname, DextoolVersion ver,
348             CustomHeader custom_hdr) {
349         auto o = CppHModule(convToIncludeGuard(fname));
350         o.header.append(makeHeader(fname, ver, custom_hdr));
351         o.content.append(hdr);
352 
353         return o;
354     }
355 
356     static auto output(CppModule code, AbsolutePath incl_fname, AbsolutePath dest,
357             DextoolVersion ver, CustomHeader custom_hdr) {
358         import std.path : baseName;
359 
360         auto o = new CppModule;
361         o.suppressIndent(1);
362         o.append(makeHeader(dest, ver, custom_hdr));
363         o.include(incl_fname.baseName);
364         o.sep(2);
365         o.append(code);
366 
367         return o;
368     }
369 
370     auto test_double_hdr = transf.createHeaderFile(null);
371 
372     foreach (k, v; gen_data.uniqueData) {
373         final switch (k) with (Code) {
374         case Kind.hdr:
375             prods.putFile(test_double_hdr, outputHdr(v,
376                     test_double_hdr, params.getToolVersion, params.getCustomHeader));
377             break;
378         case Kind.impl:
379             auto test_double_cpp = transf.createImplFile(null);
380             prods.putFile(test_double_cpp, output(v, test_double_hdr,
381                     test_double_cpp, params.getToolVersion, params.getCustomHeader));
382             break;
383         }
384     }
385 
386     auto mock_incls = new CppModule;
387     foreach (mock; gen_data.gmocks) {
388         import std.algorithm : joiner, map;
389         import std.conv : text;
390         import std.format : format;
391         import std.string : toLower;
392         import cpptooling.generator.gmock : generateGmockHdr;
393 
394         string repr_ns = mock.nesting.map!(a => a.toLower).joiner("-").text;
395         string ns_suffix = mock.nesting.length != 0 ? "-" : "";
396         auto fname = transf.createHeaderFile(format("_%s%s%s_gmock", repr_ns,
397                 ns_suffix, mock.name.toLower));
398 
399         mock_incls.include(fname.baseName);
400 
401         prods.putFile(fname, generateGmockHdr(test_double_hdr, fname,
402                 params.getToolVersion, params.getCustomHeader, mock));
403     }
404 
405     //TODO code duplication, merge with the above
406     foreach (gtest; gen_data.gtestPPHdr) {
407         import cpptooling.generator.gtest : generateGtestHdr;
408 
409         auto fname = transf.createHeaderFile(makeGtestFileName(transf, gtest.nesting, gtest.name));
410         mock_incls.include(fname.baseName);
411 
412         prods.putFile(fname, generateGtestHdr(test_double_hdr, fname,
413                 params.getToolVersion, params.getCustomHeader, gtest));
414     }
415 
416     auto gtest_impl = new CppModule;
417     gtest_impl.comment("Compile this file to automatically compile all generated pretty printer");
418     //TODO code duplication, merge with the above
419     foreach (gtest; gen_data.gtestPPImpl) {
420         auto fname_hdr = transf.createHeaderFile(makeGtestFileName(transf,
421                 gtest.nesting, gtest.name));
422         auto fname_cpp = transf.createImplFile(makeGtestFileName(transf,
423                 gtest.nesting, gtest.name));
424         gtest_impl.include(fname_cpp.baseName);
425         prods.putFile(fname_cpp, output(gtest, fname_hdr, fname_cpp,
426                 params.getToolVersion, params.getCustomHeader));
427     }
428 
429     const f_gmock_hdr = transf.createHeaderFile("_gmock");
430     if (gen_data.gmocks.length != 0 || gen_data.gtestPPHdr.length != 0) {
431         prods.putFile(f_gmock_hdr, outputHdr(mock_incls, f_gmock_hdr,
432                 params.getToolVersion, params.getCustomHeader));
433     }
434 
435     if (gen_data.gtestPPHdr.length != 0) {
436         auto fname = transf.createImplFile("_fused_gtest");
437         prods.putFile(fname, output(gtest_impl, f_gmock_hdr, fname,
438                 params.getToolVersion, params.getCustomHeader));
439     }
440 
441     if (ctrl.doPreIncludes) {
442         prods.putFile(gen_data.includeHooks.preInclude,
443                 generatePreInclude(gen_data.includeHooks.preInclude), WriteStrategy.skip);
444     }
445 
446     if (ctrl.doPostIncludes) {
447         prods.putFile(gen_data.includeHooks.postInclude,
448                 generatePostInclude(gen_data.includeHooks.postInclude), WriteStrategy.skip);
449     }
450 }
451 
452 string makeGtestFileName(Transform transf, CppNs[] nesting, CppClassName name) {
453     import std.algorithm : joiner, map;
454     import std.conv : text;
455     import std.format : format;
456     import std.string : toLower;
457 
458     string repr_ns = nesting.map!(a => a.toLower).joiner("-").text;
459     string ns_suffix = nesting.length != 0 ? "-" : "";
460     return format("_%s%s%s_gtest", repr_ns, ns_suffix, name.toLower);
461 }