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 }