1 /** 2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 */ 10 module dextool.plugin.mutate.backend.analyze.pass_mutant; 11 12 import logger = std.experimental.logger; 13 import std.algorithm : among, map, sort, filter; 14 import std.array : appender, empty, array, Appender; 15 import std.exception : collectException; 16 import std.format : formattedWrite; 17 import std.range : retro, ElementType; 18 import std.typecons : tuple, Tuple, scoped; 19 20 import dextool.type : AbsolutePath, Path; 21 22 import dextool.plugin.mutate.backend.analyze.ast; 23 import dextool.plugin.mutate.backend.analyze.internal : TokenStream; 24 import dextool.plugin.mutate.backend.analyze.utility; 25 import dextool.plugin.mutate.backend.interface_ : ValidateLoc, FilesysIO; 26 import dextool.plugin.mutate.backend.type : Language, Offset, Mutation, SourceLocRange, Token; 27 28 @safe: 29 30 /// Find mutants. 31 MutantsResult toMutants(ref Ast ast, FilesysIO fio, ValidateLoc vloc) { 32 auto visitor = () @trusted { return new MutantVisitor(&ast, fio, vloc); }(); 33 ast.accept(visitor); 34 return visitor.result; 35 } 36 37 /// Filter the mutants and add checksums. 38 CodeMutantsResult toCodeMutants(MutantsResult mutants, FilesysIO fio, TokenStream tstream) { 39 auto result = new CodeMutantsResult(mutants.lang, fio, tstream); 40 result.put(mutants.files); 41 42 foreach (f; mutants.files.map!(a => a.path)) { 43 foreach (mp; mutants.getMutationPoints(f).array.sort!((a, 44 b) => a.point.offset < b.point.offset)) { 45 // use this to easily find zero length mutants. 46 // note though that it can't always be active because it has 47 // some false positives such as dccBomb 48 //if (mp.point.offset.begin == mp.point.offset.end) { 49 // logger.warningf("Malformed mutant (begin == end), dropping. %s %s %s %s", 50 // mp.kind, mp.point.offset, mp.point.sloc, f); 51 //} 52 53 if (mp.point.offset.begin > mp.point.offset.end) { 54 logger.warningf("Malformed mutant (begin > end), dropping. %s %s %s %s", 55 mp.kind, mp.point.offset, mp.point.sloc, f); 56 } else { 57 result.put(f, mp.point.offset, mp.point.sloc, mp.kind); 58 } 59 } 60 } 61 62 return result; 63 } 64 65 class MutantsResult { 66 import dextool.plugin.mutate.backend.type : Checksum; 67 import dextool.set; 68 69 static struct MutationPoint { 70 Offset offset; 71 SourceLocRange sloc; 72 73 size_t toHash() @safe pure nothrow const @nogc { 74 return offset.toHash; 75 } 76 77 int opCmp(ref const typeof(this) rhs) @safe pure nothrow const @nogc { 78 return offset.opCmp(rhs.offset); 79 } 80 81 string toString() @safe pure const { 82 auto buf = appender!string; 83 toString(buf); 84 return buf.data; 85 } 86 87 void toString(Writer)(ref Writer w) const { 88 formattedWrite!"[%s:%s-%s:%s]:[%s:%s]"(w, sloc.begin.line, 89 sloc.begin.column, sloc.end.line, sloc.end.column, offset.begin, offset.end); 90 } 91 } 92 93 const Language lang; 94 Tuple!(AbsolutePath, "path", Checksum, "cs")[] files; 95 96 private { 97 Set!(Mutation.Kind)[MutationPoint][AbsolutePath] points; 98 Set!AbsolutePath existingFiles; 99 FilesysIO fio; 100 ValidateLoc vloc; 101 } 102 103 this(const Language lang, FilesysIO fio, ValidateLoc vloc) { 104 this.lang = lang; 105 this.fio = fio; 106 this.vloc = vloc; 107 } 108 109 Tuple!(Mutation.Kind[], "kind", MutationPoint, "point")[] getMutationPoints(AbsolutePath file) @safe pure nothrow const { 110 alias RType = ElementType!(typeof(return)); 111 return points[file].byKeyValue.map!(a => RType(a.value.toArray, a.key)).array; 112 } 113 114 private void put(AbsolutePath absp) @trusted { 115 import dextool.plugin.mutate.backend.utility : checksum; 116 117 if (!vloc.shouldMutate(absp) || absp in existingFiles) 118 return; 119 120 try { 121 auto fin = fio.makeInput(absp); 122 auto cs = checksum(fin.content); 123 124 existingFiles.add(absp); 125 files ~= ElementType!(typeof(files))(absp, cs); 126 points[absp] = (Set!(Mutation.Kind)[MutationPoint]).init; 127 } catch (Exception e) { 128 logger.warningf("%s: %s", absp, e.msg); 129 } 130 } 131 132 private void put(AbsolutePath p, MutationPoint mp, Mutation.Kind kind) @safe { 133 if (auto a = p in points) { 134 if (auto b = mp in *a) { 135 (*b).add(kind); 136 } else { 137 Set!(Mutation.Kind) s; 138 s.add(kind); 139 (*a)[mp] = s; 140 } 141 } 142 } 143 144 override string toString() @safe { 145 import std.range : put; 146 147 auto w = appender!string(); 148 149 formattedWrite(w, "Files:\n"); 150 foreach (f; files) { 151 formattedWrite!"%s %s\n"(w, f.path, f.cs); 152 153 foreach (mp; points[f.path].byKeyValue 154 .map!(a => tuple!("key", "value")(a.key, a.value)) 155 .array 156 .sort!((a, b) => a.key < b.key)) { 157 formattedWrite(w, " %s->%s\n", mp.key, mp.value.toRange); 158 } 159 } 160 161 return w.data; 162 } 163 } 164 165 /** 166 * The design of the API have some calling requirements that do not make it 167 * suitable for general consumtion. It is deemed OK as long as those methods 168 * are private and used in only one place. 169 */ 170 class CodeMutantsResult { 171 import dextool.hash : Checksum128, BuildChecksum128, toBytes, toChecksum128; 172 import dextool.plugin.mutate.backend.type : CodeMutant, CodeChecksum, Mutation, Checksum; 173 import dextool.plugin.mutate.backend.analyze.id_factory : MutationIdFactory; 174 175 const Language lang; 176 Tuple!(AbsolutePath, "path", Checksum, "cs")[] files; 177 178 static struct MutationPoint { 179 CodeMutant[] mutants; 180 Offset offset; 181 SourceLocRange sloc; 182 183 size_t toHash() @safe pure nothrow const @nogc { 184 return offset.toHash; 185 } 186 187 int opCmp(ref const typeof(this) rhs) @safe pure nothrow const @nogc { 188 return offset.opCmp(rhs.offset); 189 } 190 191 string toString() @safe pure const { 192 auto buf = appender!string; 193 toString(buf); 194 return buf.data; 195 } 196 197 void toString(Writer)(ref Writer w) const { 198 formattedWrite!"[%s:%s-%s:%s]:[%s:%s]"(w, sloc.begin.line, 199 sloc.begin.column, sloc.end.line, sloc.end.column, offset.begin, offset.end); 200 } 201 } 202 203 MutationPoint[][AbsolutePath] points; 204 Checksum[AbsolutePath] csFiles; 205 206 private { 207 FilesysIO fio; 208 209 TokenStream tstream; 210 211 MutationIdFactory idFactory; 212 /// Tokens of the current file that idFactory is configured for. 213 Token[] tokens; 214 } 215 216 this(Language lang, FilesysIO fio, TokenStream tstream) { 217 this.lang = lang; 218 this.fio = fio; 219 this.tstream = tstream; 220 } 221 222 private void put(typeof(MutantsResult.files) mfiles) { 223 files = mfiles; 224 foreach (f; files) { 225 csFiles[f.path] = f.cs; 226 points[f.path] = (MutationPoint[]).init; 227 } 228 } 229 230 /// Returns: a tuple of two elements. The tokens before and after the mutation point. 231 private static auto splitByMutationPoint(Token[] toks, Offset offset) { 232 import std.algorithm : countUntil; 233 import std.typecons : Tuple; 234 235 Tuple!(size_t, "pre", size_t, "post") rval; 236 237 const pre_idx = toks.countUntil!((a, b) => a.offset.begin > b.begin)(offset); 238 if (pre_idx == -1) { 239 rval.pre = toks.length; 240 return rval; 241 } 242 243 rval.pre = pre_idx; 244 toks = toks[pre_idx .. $]; 245 246 const post_idx = toks.countUntil!((a, b) => a.offset.end > b.end)(offset); 247 if (post_idx != -1) { 248 rval.post = toks.length - post_idx; 249 } 250 251 return rval; 252 } 253 254 // the mutation points must be added in sorted order by their offset 255 private void put(AbsolutePath p, Offset offset, SourceLocRange sloc, Mutation.Kind[] kinds) @safe { 256 import dextool.plugin.mutate.backend.generate_mutant : makeMutationText; 257 258 if (p != idFactory.fileName) { 259 tokens = tstream.getFilteredTokens(p); 260 idFactory = MutationIdFactory(p, csFiles[p], tokens); 261 } 262 263 auto split = splitByMutationPoint(tokens, offset); 264 265 idFactory.updatePosition(split.pre, split.post); 266 267 auto fin = fio.makeInput(p); 268 auto cmuts = appender!(CodeMutant[])(); 269 foreach (kind; kinds) { 270 auto txt = makeMutationText(fin, offset, kind, lang); 271 auto cm = idFactory.makeMutant(Mutation(kind), txt.rawMutation); 272 cmuts.put(cm); 273 } 274 275 points[p] ~= MutationPoint(cmuts.data, offset, sloc); 276 } 277 278 override string toString() @safe { 279 import std.range : put; 280 281 auto w = appender!string(); 282 283 formattedWrite(w, "Files:\n"); 284 foreach (f; files) { 285 formattedWrite!"%s %s\n"(w, f.path, f.cs); 286 287 foreach (mp; points[f.path]) { 288 formattedWrite!" %s->%s\n"(w, mp, mp.mutants); 289 } 290 } 291 292 return w.data; 293 } 294 } 295 296 private: 297 298 // Mutation 299 300 /** 301 * TODO: remove OpTypeInfo. It is just used to reduce the impact of the 302 * refactoring. 303 */ 304 class MutantVisitor : DepthFirstVisitor { 305 import dextool.plugin.mutate.backend.mutation_type.abs : absMutations; 306 import dextool.plugin.mutate.backend.mutation_type.dcc : dccMutations; 307 import dextool.plugin.mutate.backend.mutation_type.dcr : dcrMutations; 308 import dextool.plugin.mutate.backend.mutation_type.sdl : stmtDelMutations; 309 310 Ast* ast; 311 MutantsResult result; 312 313 private { 314 uint depth; 315 Stack!(Node) nstack; 316 } 317 318 this(Ast* ast, FilesysIO fio, ValidateLoc vloc) { 319 this.ast = ast; 320 result = new MutantsResult(ast.lang, fio, vloc); 321 322 // by adding the locations here the rest of the visitor do not have to 323 // be concerned about adding files. 324 foreach (ref l; ast.locs.byValue) { 325 result.put(l.file); 326 } 327 } 328 329 void visitPush(Node n) { 330 nstack.put(n, ++depth); 331 } 332 333 void visitPop() { 334 nstack.pop; 335 --depth; 336 } 337 338 /// Returns: true if the current node is inside a function that returns bool. 339 bool isInsideBoolFunc() { 340 if (auto rty = closestFuncType) { 341 if (rty.kind == TypeKind.boolean) { 342 return true; 343 } 344 } 345 return false; 346 } 347 348 /// Returns: the depth (1+) if any of the parent nodes is `k`. 349 uint isInside(Kind k) { 350 return nstack.isInside(k); 351 } 352 353 /// Returns: the type, if any, of the function that the current visited node is inside. 354 auto closestFuncType() @trusted { 355 foreach (n; nstack[].retro.filter!(a => a.data.kind == Kind.Function)) { 356 auto fn = cast(Function) n.data; 357 if (auto rty = ast.type(fn.return_)) { 358 return rty; 359 } 360 break; 361 } 362 return null; 363 } 364 365 void put(Location loc, Mutation.Kind[] kinds, const bool blacklist) { 366 if (blacklist) 367 return; 368 369 foreach (kind; kinds) { 370 result.put(loc.file, MutantsResult.MutationPoint(loc.interval, loc.sloc), kind); 371 } 372 } 373 374 alias visit = DepthFirstVisitor.visit; 375 376 override void visit(Expr n) { 377 auto loc = ast.location(n); 378 put(loc, absMutations(n.kind), n.blacklist); 379 380 if (isInsideBoolFunc && isInside(Kind.Return) && !isInside(Kind.Call)) { 381 put(loc, dccMutations(n.kind), n.blacklist); 382 put(loc, dcrMutations(n.kind), n.blacklist); 383 } 384 385 accept(n, this); 386 } 387 388 override void visit(Function n) { 389 accept(n, this); 390 } 391 392 override void visit(Block n) @trusted { 393 auto sdlAnalyze = scoped!SdlBlockVisitor(ast); 394 sdlAnalyze.startVisit(n); 395 if (sdlAnalyze.canRemove) { 396 put(sdlAnalyze.loc, stmtDelMutations(n.kind), n.blacklist); 397 } 398 399 accept(n, this); 400 } 401 402 override void visit(Call n) { 403 auto loc = ast.location(n); 404 405 if (ast.type(n) is null && !isInside(Kind.Return)) { 406 // the check for Return blocks all SDL when an exception is thrown. 407 // 408 // a bit restricive to be begin with to only delete void returning 409 // functions. Extend it in the future when it can "see" that the 410 // return value is discarded. 411 put(loc, stmtDelMutations(n.kind), n.blacklist); 412 } 413 414 if (isInsideBoolFunc && isInside(Kind.Return)) { 415 put(loc, dccMutations(n.kind), n.blacklist); 416 put(loc, dcrMutations(n.kind), n.blacklist); 417 } 418 419 // should call visitOp 420 421 accept(n, this); 422 } 423 424 override void visit(Return n) { 425 if (closestFuncType is null) { 426 // if the function return void then it is safe to delete the return. 427 // 428 // c++ throw expressions is modelled as returns with a child node 429 // Call. Overall in any language it should be OK to remove a return 430 // of something that returns void, the bottom type. 431 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 432 } 433 434 accept(n, this); 435 } 436 437 override void visit(OpAssign n) { 438 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 439 accept(n, this); 440 } 441 442 override void visit(OpAssignAdd n) { 443 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 444 accept(n, this); 445 } 446 447 override void visit(OpAssignAndBitwise n) { 448 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 449 accept(n, this); 450 } 451 452 override void visit(OpAssignDiv n) { 453 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 454 accept(n, this); 455 } 456 457 override void visit(OpAssignMod n) { 458 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 459 accept(n, this); 460 } 461 462 override void visit(OpAssignMul n) { 463 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 464 accept(n, this); 465 } 466 467 override void visit(OpAssignOrBitwise n) { 468 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 469 accept(n, this); 470 } 471 472 override void visit(OpAssignSub n) { 473 put(ast.location(n), stmtDelMutations(n.kind), n.blacklist); 474 accept(n, this); 475 } 476 477 override void visit(OpNegate n) { 478 visitUnaryOp(n); 479 accept(n, this); 480 } 481 482 override void visit(OpAndBitwise n) { 483 visitComparisonBinaryOp(n); 484 accept(n, this); 485 } 486 487 override void visit(OpAnd n) { 488 visitComparisonBinaryOp(n); 489 accept(n, this); 490 } 491 492 override void visit(OpOrBitwise n) { 493 visitComparisonBinaryOp(n); 494 accept(n, this); 495 } 496 497 override void visit(OpOr n) { 498 visitComparisonBinaryOp(n); 499 accept(n, this); 500 } 501 502 override void visit(OpLess n) { 503 visitComparisonBinaryOp(n); 504 accept(n, this); 505 } 506 507 override void visit(OpLessEq n) { 508 visitComparisonBinaryOp(n); 509 accept(n, this); 510 } 511 512 override void visit(OpGreater n) { 513 visitComparisonBinaryOp(n); 514 accept(n, this); 515 } 516 517 override void visit(OpGreaterEq n) { 518 visitComparisonBinaryOp(n); 519 accept(n, this); 520 } 521 522 override void visit(OpEqual n) { 523 visitComparisonBinaryOp(n); 524 accept(n, this); 525 } 526 527 override void visit(OpNotEqual n) { 528 visitComparisonBinaryOp(n); 529 accept(n, this); 530 } 531 532 override void visit(OpAdd n) { 533 visitArithmeticBinaryOp(n); 534 accept(n, this); 535 } 536 537 override void visit(OpSub n) { 538 visitArithmeticBinaryOp(n); 539 accept(n, this); 540 } 541 542 override void visit(OpMul n) { 543 visitArithmeticBinaryOp(n); 544 accept(n, this); 545 } 546 547 override void visit(OpMod n) { 548 visitArithmeticBinaryOp(n); 549 accept(n, this); 550 } 551 552 override void visit(OpDiv n) { 553 visitArithmeticBinaryOp(n); 554 accept(n, this); 555 } 556 557 override void visit(Condition n) { 558 auto kinds = dccMutations(n.kind); 559 kinds ~= dcrMutations(n.kind); 560 put(ast.location(n), kinds, n.blacklist); 561 accept(n, this); 562 } 563 564 override void visit(Branch n) { 565 // only case statements have an inside. pretty bad "detection" but 566 // works for now. 567 if (n.inside !is null) { 568 // removing the whole branch because then e.g. a switch-block would 569 // jump to the default branch. It becomes "more" predictable what 570 // happens compared to "falling through to the next case". 571 put(ast.location(n), dcrMutations(n.kind), n.blacklist); 572 573 put(ast.location(n.inside), dccMutations(n.kind), n.inside.blacklist); 574 } 575 accept(n, this); 576 } 577 578 private void visitUnaryOp(T)(T n) { 579 auto loc = ast.location(n); 580 auto locExpr = ast.location(n.expr); 581 auto locOp = ast.location(n.operator); 582 583 Mutation.Kind[] expr, op; 584 585 { 586 import dextool.plugin.mutate.backend.mutation_type.uoi; 587 588 op ~= uoiMutations(n.kind); 589 } 590 { 591 expr ~= absMutations(n.kind); 592 } 593 594 put(loc, expr, n.blacklist); 595 put(locOp, op, n.operator.blacklist); 596 } 597 598 private void visitComparisonBinaryOp(T)(T n) { 599 Mutation.Kind[] expr, op, lhs, rhs; 600 601 { 602 import dextool.plugin.mutate.backend.mutation_type.ror; 603 604 auto m = rorMutations(RorInfo(n.kind, ast.type(n.lhs), 605 ast.symbol(n.lhs), ast.type(n.rhs), ast.symbol(n.rhs))); 606 expr ~= m.expr; 607 op ~= m.op; 608 } 609 { 610 import dextool.plugin.mutate.backend.mutation_type.lcr; 611 612 auto m = lcrMutations(n.kind); 613 expr ~= m.expr; 614 op ~= m.op; 615 lhs ~= m.lhs; 616 rhs ~= m.rhs; 617 } 618 { 619 import dextool.plugin.mutate.backend.mutation_type.lcrb; 620 621 auto m = lcrbMutations(n.kind); 622 op ~= m.op; 623 lhs ~= m.lhs; 624 rhs ~= m.rhs; 625 } 626 { 627 expr ~= absMutations(n.kind); 628 } 629 { 630 expr ~= dcrMutations(n.kind); 631 expr ~= dccMutations(n.kind); 632 } 633 634 visitBinaryOp(n, op, lhs, rhs, expr); 635 } 636 637 private void visitArithmeticBinaryOp(T)(T n) { 638 Mutation.Kind[] op, lhs, rhs, expr; 639 640 { 641 expr ~= absMutations(n.kind); 642 } 643 { 644 import dextool.plugin.mutate.backend.mutation_type.aor; 645 646 auto m = aorMutations(n.kind); 647 op ~= m.op; 648 lhs ~= m.lhs; 649 rhs ~= m.rhs; 650 } 651 652 visitBinaryOp(n, op, lhs, rhs, expr); 653 } 654 655 private void visitBinaryOp(T)(T n, Mutation.Kind[] op, Mutation.Kind[] lhs, 656 Mutation.Kind[] rhs, Mutation.Kind[] expr) { 657 auto locExpr = ast.location(n); 658 auto locOp = ast.location(n.operator); 659 660 put(locExpr, expr, n.blacklist); 661 put(locOp, op, n.operator.blacklist); 662 // the interval check: 663 // != is sufficiently for unary operators such as ++. 664 // but there are also malformed that can be created thus '<' is needed. 665 // Change this to != and run on the game tutorial to see them being 666 // produced. Seems to be something with templates and function calls. 667 if (n.lhs !is null && locExpr.interval.begin < locOp.interval.end) { 668 auto offset = Interval(locExpr.interval.begin, locOp.interval.end); 669 put(new Location(locOp.file, offset, 670 SourceLocRange(locExpr.sloc.begin, locOp.sloc.end)), lhs, n.lhs.blacklist); 671 } 672 if (n.rhs !is null && locOp.interval.begin < locExpr.interval.end) { 673 auto offset = Interval(locOp.interval.begin, locExpr.interval.end); 674 put(new Location(locOp.file, offset, 675 SourceLocRange(locOp.sloc.begin, locExpr.sloc.end)), rhs, n.rhs.blacklist); 676 } 677 } 678 } 679 680 /** Analyze a block to see if its content can be removed by the SDL mutation 681 * operator. 682 * 683 * The block must: 684 * * contain something. 685 * * not contain a `Return` that returns a type other than void. 686 */ 687 class SdlBlockVisitor : DepthFirstVisitor { 688 Ast* ast; 689 690 // if the analyzer has determined that this node in the tree can be removed 691 // with SDL. Note though that it doesn't know anything about the parent 692 // node. 693 bool canRemove; 694 // if the block contains returns; 695 bool hasReturn; 696 /// The location that represent the block to remove. 697 Location loc; 698 699 this(Ast* ast) { 700 this.ast = ast; 701 } 702 703 /// The node to start analysis from. 704 void startVisit(Block n) { 705 auto l = ast.location(n); 706 707 if (l.interval.end.among(l.interval.begin, l.interval.begin + 1)) { 708 // it is an empty block so it can't be removed. 709 return; 710 } 711 712 if (l.interval.begin < l.interval.end) { 713 loc = l; 714 canRemove = true; 715 accept(n, this); 716 } 717 } 718 719 alias visit = DepthFirstVisitor.visit; 720 721 override void visit(Block n) { 722 accept(n, this); 723 } 724 725 override void visit(Return n) { 726 hasReturn = true; 727 728 // a return expression that is NOT void always have a child which is 729 // the value returned. 730 if (!n.children.empty) { 731 canRemove = false; 732 } else { 733 accept(n, this); 734 } 735 } 736 }