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 }