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 }