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 }