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 }