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;
19 
20 static import colorlog;
21 
22 import dextool.type : AbsolutePath, Path;
23 
24 import dextool.plugin.mutate.backend.analyze.ast;
25 import dextool.plugin.mutate.backend.analyze.internal : TokenStream;
26 import dextool.plugin.mutate.backend.analyze.utility;
27 import dextool.plugin.mutate.backend.interface_ : ValidateLoc, FilesysIO;
28 import dextool.plugin.mutate.backend.type : Language, Offset, Mutation, SourceLocRange, Token;
29 import dextool.plugin.mutate.type : MutantIdGeneratorConfig;
30 
31 alias log = colorlog.log!"analyze.pass_mutant";
32 
33 shared static this() {
34     colorlog.make!(colorlog.SimpleLogger)(logger.LogLevel.info, "analyze.pass_mutant");
35 }
36 
37 @safe:
38 
39 /// Find mutants.
40 MutantsResult toMutants(Ast* ast, FilesysIO fio, ValidateLoc vloc, Mutation.Kind[] kinds) {
41     scope visitor = new MutantVisitor(ast, fio, vloc, kinds);
42     () @trusted { ast.accept(visitor); }();
43     return visitor.result;
44 }
45 
46 /// Filter the mutants and add checksums.
47 CodeMutantsResult toCodeMutants(MutantsResult mutants, FilesysIO fio,
48         scope TokenStream tstream, MutantIdGeneratorConfig idGenConf) {
49     auto result = new CodeMutantsResult(mutants.lang, fio, idGenConf);
50     result.put(mutants.files);
51 
52     foreach (f; mutants.files.map!(a => a.path).array.sort) {
53         foreach (mp; mutants.getMutationPoints(f).filter!(a => !a.kind.empty)) {
54             // use this to easily find zero length mutants.
55             // note though that it can't always be active because it has
56             // some false positives such as dccBomb
57             //if (mp.point.offset.begin == mp.point.offset.end) {
58             //    logger.warningf("Malformed mutant (begin == end), dropping. %s %s %s %s",
59             //            mp.kind, mp.point.offset, mp.point.sloc, f);
60             //}
61 
62             if (mp.point.offset.begin > mp.point.offset.end) {
63                 log.warningf("Malformed mutant (begin > end), dropping. %s %s %s %s",
64                         mp.kind, mp.point.offset, mp.point.sloc, f);
65             } else {
66                 result.updateContentWindow(mp.point.context);
67                 result.changeActiveFile(f, tstream);
68                 result.put(f, mp.point.offset, mp.point.sloc, mp.kind);
69             }
70         }
71     }
72 
73     // logger.info(result);
74 
75     return result;
76 }
77 
78 class MutantsResult {
79     import dextool.plugin.mutate.backend.type : Checksum;
80     import my.set;
81 
82     static struct MutationPoint {
83         Offset offset;
84         SourceLocRange sloc;
85         Offset context;
86 
87         size_t toHash() @safe pure nothrow const @nogc {
88             return offset.toHash;
89         }
90 
91         int opCmp(ref const typeof(this) rhs) @safe pure nothrow const @nogc {
92             return offset.opCmp(rhs.offset);
93         }
94 
95         string toString() @safe pure const {
96             auto buf = appender!string;
97             toString(buf);
98             return buf.data;
99         }
100 
101         void toString(Writer)(ref Writer w) const {
102             formattedWrite!"[%s:%s-%s:%s]:[%s:%s][%s:%s]"(w, sloc.begin.line,
103                     sloc.begin.column, sloc.end.line, sloc.end.column,
104                     offset.begin, offset.end, context.begin, context.end);
105         }
106     }
107 
108     const Language lang;
109     Tuple!(AbsolutePath, "path", Checksum, "cs")[] files;
110 
111     private {
112         Set!(Mutation.Kind)[MutationPoint][AbsolutePath] points;
113         Set!AbsolutePath existingFiles;
114         FilesysIO fio;
115         ValidateLoc vloc;
116         Set!(Mutation.Kind) kinds;
117     }
118 
119     this(const Language lang, FilesysIO fio, ValidateLoc vloc, Mutation.Kind[] kinds) {
120         this.lang = lang;
121         this.fio = fio;
122         this.vloc = vloc;
123         this.kinds = toSet(kinds);
124     }
125 
126     Tuple!(Mutation.Kind[], "kind", MutationPoint, "point")[] getMutationPoints(AbsolutePath file) @safe pure nothrow const {
127         alias RType = ElementType!(typeof(return));
128         return points[file].byKeyValue.map!(a => RType(a.value.toArray, a.key)).array;
129     }
130 
131     /// Drop a mutant.
132     void drop(AbsolutePath p, MutationPoint mp, Mutation.Kind kind) @safe {
133         if (auto a = p in points) {
134             if (auto b = mp in *a) {
135                 (*b).remove(kind);
136                 if (b.empty)
137                     (*a).remove(mp);
138             }
139         }
140     }
141 
142     private void put(AbsolutePath absp) @trusted {
143         import dextool.plugin.mutate.backend.utility : checksum;
144 
145         if (!vloc.shouldMutate(absp) || absp in existingFiles)
146             return;
147 
148         try {
149             auto fin = fio.makeInput(absp);
150             auto cs = checksum(fin.content);
151 
152             existingFiles.add(absp);
153             files ~= ElementType!(typeof(files))(absp, cs);
154             points[absp] = (Set!(Mutation.Kind)[MutationPoint]).init;
155         } catch (Exception e) {
156             log.warningf("%s: %s", absp, e.msg);
157         }
158     }
159 
160     private void put(AbsolutePath p, MutationPoint mp, Mutation.Kind kind) @safe {
161         if (kind !in kinds) {
162             return;
163         }
164 
165         if (auto a = p in points) {
166             if (auto b = mp in *a) {
167                 (*b).add(kind);
168             } else {
169                 Set!(Mutation.Kind) s;
170                 s.add(kind);
171                 (*a)[mp] = s;
172             }
173         }
174     }
175 
176     override string toString() @safe {
177         import std.range : put;
178 
179         auto w = appender!string();
180 
181         put(w, "MutantsResult\n");
182         formattedWrite(w, "Files:\n");
183         foreach (f; files) {
184             formattedWrite!"%s %s\n"(w, f.path, f.cs);
185 
186             foreach (mp; points[f.path].byKeyValue
187                     .map!(a => tuple!("key", "value")(a.key, a.value))
188                     .array
189                     .sort!((a, b) => a.key < b.key)) {
190                 formattedWrite(w, "  %s->%s\n", mp.key, mp.value.toRange);
191             }
192         }
193 
194         return w.data;
195     }
196 }
197 
198 /**
199  * The design of the API have some calling requirements that do not make it
200  * suitable for general consumtion. It is deemed OK as long as those methods
201  * are private and used in only one place.
202  */
203 class CodeMutantsResult {
204     import my.hash : Checksum64, BuildChecksum64, toBytes, toChecksum64;
205     import dextool.plugin.mutate.backend.type : CodeMutant, CodeChecksum, Mutation, Checksum;
206     import dextool.plugin.mutate.backend.analyze.id_factory : MutantIdFactory;
207 
208     const Language lang;
209     Tuple!(AbsolutePath, "path", Checksum, "cs")[] files;
210 
211     static struct MutationPoint {
212         CodeMutant mutant;
213         Offset offset;
214         SourceLocRange sloc;
215 
216         size_t toHash() @safe pure nothrow const @nogc {
217             return offset.toHash;
218         }
219 
220         int opCmp(ref const typeof(this) rhs) @safe pure nothrow const @nogc {
221             return offset.opCmp(rhs.offset);
222         }
223 
224         string toString() @safe pure const {
225             auto buf = appender!string;
226             toString(buf);
227             return buf.data;
228         }
229 
230         void toString(Writer)(ref Writer w) const {
231             formattedWrite!"[%s:%s-%s:%s]:[%s:%s]"(w, sloc.begin.line,
232                     sloc.begin.column, sloc.end.line, sloc.end.column, offset.begin, offset.end);
233         }
234     }
235 
236     MutationPoint[][AbsolutePath] points;
237     Checksum[AbsolutePath] csFiles;
238 
239     private {
240         FilesysIO fio;
241 
242         /// Current filename that the id factory is initialized with.
243         AbsolutePath idFileName;
244         MutantIdFactory idFactory;
245 
246         /// Tokens of the current file that idFactory is configured for.
247         Token[] tokens;
248 
249         Offset contentWindow;
250     }
251 
252     this(Language lang, FilesysIO fio, MutantIdGeneratorConfig idGenConf) {
253         this.lang = lang;
254         this.fio = fio;
255         this.idFactory = () {
256             import dextool.plugin.mutate.backend.analyze.id_factory : StrictImpl, RelaxedImpl;
257 
258             final switch (idGenConf) {
259             case MutantIdGeneratorConfig.strict:
260                 return cast(MutantIdFactory) new StrictImpl;
261             case MutantIdGeneratorConfig.relaxed:
262                 return cast(MutantIdFactory) new RelaxedImpl;
263             }
264         }();
265     }
266 
267     /// Expected to be updated for each new mutation point.
268     void updateContentWindow(const Offset v) {
269         contentWindow = v;
270     }
271 
272     private void changeActiveFile(AbsolutePath p, scope TokenStream tstream) @trusted {
273         if (p == idFileName)
274             return;
275 
276         idFileName = p;
277         tokens = () @trusted { return tstream.getFilteredTokens(p); }();
278         idFactory.changeFile(fio.toRelativeRoot(p), tokens);
279     }
280 
281     private void put(typeof(MutantsResult.files) mfiles) {
282         files = mfiles;
283         foreach (f; files) {
284             csFiles[f.path] = f.cs;
285             points[f.path] = (MutationPoint[]).init;
286         }
287     }
288 
289     // the mutation points must be added in sorted order by their offset
290     private void put(AbsolutePath p, Offset offset, SourceLocRange sloc, Mutation.Kind[] kinds) @safe {
291         import dextool.plugin.mutate.backend.generate_mutant : makeMutationText;
292 
293         idFactory.update(contentWindow, offset, tokens);
294 
295         auto fin = fio.makeInput(p);
296         auto muts = appender!(MutationPoint[])();
297         foreach (kind; kinds) {
298             scope txt = makeMutationText(fin, offset, kind, lang);
299             auto cm = idFactory.make(Mutation(kind), txt.rawMutation);
300             muts.put(MutationPoint(cm, offset, sloc));
301         }
302         points[p] ~= muts.data;
303     }
304 
305     override string toString() @safe {
306         import std.range : put;
307 
308         auto w = appender!string();
309 
310         formattedWrite(w, "Files:\n");
311         foreach (f; files) {
312             formattedWrite!"%s %s\n"(w, f.path, f.cs);
313 
314             foreach (mp; points[f.path]) {
315                 formattedWrite!"  %s->%s\n"(w, mp, mp.mutant);
316             }
317         }
318 
319         return w.data;
320     }
321 }
322 
323 private:
324 
325 class MutantVisitor : DepthFirstVisitor {
326     import dextool.plugin.mutate.backend.mutation_type.dcr : dcrMutations, DcrInfo;
327     import dextool.plugin.mutate.backend.mutation_type.sdl : stmtDelMutations;
328 
329     Ast* ast;
330     MutantsResult result;
331     Offset context;
332 
333     private {
334         uint depth;
335         Stack!(Node) nstack;
336     }
337 
338     alias visit = DepthFirstVisitor.visit;
339 
340     this(Ast* ast, FilesysIO fio, ValidateLoc vloc, Mutation.Kind[] kinds) {
341         this.ast = ast;
342         result = new MutantsResult(ast.lang, fio, vloc, kinds);
343 
344         // by adding the locations here the rest of the visitor do not have to
345         // be concerned about adding files.
346         foreach (ref l; ast.locs.byValue) {
347             result.put(l.file);
348         }
349     }
350 
351     override void visitPush(Node n) {
352         nstack.put(n, ++depth);
353     }
354 
355     override void visitPop(Node n) {
356         nstack.pop;
357         --depth;
358     }
359 
360     /// Returns: the closest function from the current node.
361     Function getClosestFunc() {
362         return cast(Function) match!((a) {
363             if (a.data.kind == Kind.Function)
364                 return a.data;
365             return null;
366         })(nstack, Direction.topToBottom);
367     }
368 
369     /// Returns: true if the current node is inside a function that returns bool.
370     bool isParentBoolFunc() {
371         if (auto rty = closestFuncType) {
372             if (rty.kind == TypeKind.boolean) {
373                 return true;
374             }
375         }
376         return false;
377     }
378 
379     /// Returns: the depth (1+) if any of the parent nodes is `k`.
380     uint isParent(Kind k) {
381         return nstack.isParent(k);
382     }
383 
384     /// Returns: if the previous nodes is of kind `k`.
385     bool isDirectParent(Kind k) {
386         if (nstack.empty)
387             return false;
388         return nstack.back.kind == k;
389     }
390 
391     /// Returns: the type, if any, of the function that the current visited node is inside.
392     Type closestFuncType() @trusted {
393         auto f = getClosestFunc;
394         if (f is null)
395             return null;
396         return ast.type(f.return_);
397     }
398 
399     /// Returns: a range of all types of the children of `n`.
400     auto allTypes(Node n) {
401         return n.children
402             .map!(a => ast.type(a))
403             .filter!(a => a !is null);
404     }
405 
406     /// Returns: a range of all kinds of the children of `n`.
407     void put(Location loc, Mutation.Kind[] kinds, const bool blacklist) {
408         if (blacklist)
409             return;
410 
411         foreach (kind; kinds) {
412             result.put(loc.file, MutantsResult.MutationPoint(loc.interval,
413                     loc.sloc, context), kind);
414         }
415     }
416 
417     override void visit(TranslationUnit n) {
418         context = ast.location(n).interval;
419         accept(n, this);
420     }
421 
422     override void visit(Constructor n) {
423         accept(n, this);
424     }
425 
426     override void visit(Function n) {
427         auto old = context;
428         scope (exit)
429             context = old;
430         if (n.context)
431             context = ast.location(n).interval;
432         accept(n, this);
433     }
434 
435     override void visit(Expr n) {
436         auto loc = ast.location(n);
437 
438         if (isParentBoolFunc && isParent(Kind.Return) && !isParent(Kind.Call)) {
439             put(loc, dcrMutations(DcrInfo(n.kind, ast.type(n))), n.blacklist);
440         }
441 
442         accept(n, this);
443     }
444 
445     override void visit(Literal n) {
446         import dextool.plugin.mutate.backend.mutation_type.cr;
447 
448         auto loc = ast.location(n);
449         put(loc, crMutationsAll.dup, n.blacklist);
450         accept(n, this);
451     }
452 
453     override void visit(Block n) {
454         sdlBlock(n, ast.location(n), stmtDelMutations(n.kind));
455         accept(n, this);
456     }
457 
458     override void visit(Loop n) {
459         sdlBlock(n, ast.location(n), stmtDelMutations(n.kind));
460         accept(n, this);
461     }
462 
463     override void visit(BranchBundle n) {
464         sdlBlock(n, ast.location(n), stmtDelMutations(n.kind));
465         accept(n, this);
466     }
467 
468     override void visit(Call n) {
469         // the check isParent..:
470         // e.g. a C++ class constructor calls a members constructor in its
471         // initialization list.
472         // TODO: is this needed? I do not think so considering the rest of the
473         // code.
474 
475         if (isParent(Kind.Function) && !isParent(Kind.Return) && isDirectParent(Kind.Block)) {
476             // the check for Return blocks all SDL when an exception is thrown.
477             //
478             // the check isDirectParent(Kind.Block) is to only delete function
479             // or method calls that are at the root of a chain of calls in the
480             // AST.
481             //
482             // a bit restricive to be begin with to only delete void returning
483             // functions. Extend it in the future when it can "see" that the
484             // return value is discarded.
485             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
486         }
487 
488         if (isDirectParent(Kind.Return) && isParentBoolFunc) {
489             put(ast.location(n), dcrMutations(DcrInfo(n.kind, ast.type(n))), n.blacklist);
490         }
491 
492         // should call visitOp
493         accept(n, this);
494     }
495 
496     override void visit(Return n) {
497         auto ty = closestFuncType;
498 
499         if (ty !is null && ty.kind == TypeKind.top) {
500             auto loc = ast.location(n);
501             // only function with return type void (top, no type) can be
502             // deleted without introducing undefined behavior.
503             put(loc, stmtDelMutations(n.kind), n.blacklist);
504         }
505 
506         accept(n, this);
507     }
508 
509     override void visit(BinaryOp n) {
510         if (isDirectParent(Kind.Block)) {
511             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
512         }
513         accept(n, this);
514     }
515 
516     override void visit(Poison n) {
517         auto old = context;
518         scope (exit)
519             context = old;
520         if (n.context)
521             context = ast.location(n).interval;
522         accept(n, this);
523     }
524 
525     override void visit(OpAssign n) {
526         if (isDirectParent(Kind.Block)) {
527             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
528         }
529         accept(n, this);
530     }
531 
532     override void visit(OpAssignAdd n) {
533         if (isDirectParent(Kind.Block)) {
534             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
535         }
536         accept(n, this);
537     }
538 
539     override void visit(OpAssignAndBitwise n) {
540         if (isDirectParent(Kind.Block)) {
541             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
542         }
543         accept(n, this);
544     }
545 
546     override void visit(OpAssignDiv n) {
547         if (isDirectParent(Kind.Block)) {
548             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
549         }
550         accept(n, this);
551     }
552 
553     override void visit(OpAssignMod n) {
554         if (isDirectParent(Kind.Block)) {
555             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
556         }
557         accept(n, this);
558     }
559 
560     override void visit(OpAssignMul n) {
561         if (isDirectParent(Kind.Block)) {
562             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
563         }
564         accept(n, this);
565     }
566 
567     override void visit(OpAssignOrBitwise n) {
568         if (isDirectParent(Kind.Block)) {
569             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
570         }
571         accept(n, this);
572     }
573 
574     override void visit(OpAssignSub n) {
575         if (isDirectParent(Kind.Block)) {
576             put(ast.location(n), stmtDelMutations(n.kind), n.blacklist);
577         }
578         accept(n, this);
579     }
580 
581     override void visit(OpNegate n) {
582         visitUnaryOp(n);
583         accept(n, this);
584     }
585 
586     override void visit(OpAndBitwise n) {
587         visitComparisonBinaryOp(n);
588         accept(n, this);
589     }
590 
591     override void visit(OpAnd n) {
592         visitComparisonBinaryOp(n);
593         accept(n, this);
594     }
595 
596     override void visit(OpOrBitwise n) {
597         visitComparisonBinaryOp(n);
598         accept(n, this);
599     }
600 
601     override void visit(OpOr n) {
602         visitComparisonBinaryOp(n);
603         accept(n, this);
604     }
605 
606     override void visit(OpLess n) {
607         visitComparisonBinaryOp(n);
608         accept(n, this);
609     }
610 
611     override void visit(OpLessEq n) {
612         visitComparisonBinaryOp(n);
613         accept(n, this);
614     }
615 
616     override void visit(OpGreater n) {
617         visitComparisonBinaryOp(n);
618         accept(n, this);
619     }
620 
621     override void visit(OpGreaterEq n) {
622         visitComparisonBinaryOp(n);
623         accept(n, this);
624     }
625 
626     override void visit(OpEqual n) {
627         visitComparisonBinaryOp(n);
628         accept(n, this);
629     }
630 
631     override void visit(OpNotEqual n) {
632         visitComparisonBinaryOp(n);
633         accept(n, this);
634     }
635 
636     override void visit(OpAdd n) {
637         visitArithmeticBinaryOp(n);
638         accept(n, this);
639     }
640 
641     override void visit(OpSub n) {
642         visitArithmeticBinaryOp(n);
643         accept(n, this);
644     }
645 
646     override void visit(OpMul n) {
647         visitArithmeticBinaryOp(n);
648         accept(n, this);
649     }
650 
651     override void visit(OpMod n) {
652         visitArithmeticBinaryOp(n);
653         accept(n, this);
654     }
655 
656     override void visit(OpDiv n) {
657         visitArithmeticBinaryOp(n);
658         accept(n, this);
659     }
660 
661     override void visit(Condition n) {
662         put(ast.location(n), dcrMutations(DcrInfo(n.kind, ast.type(n))), n.blacklist);
663         accept(n, this);
664     }
665 
666     override void visit(Branch n) {
667         // only case statements have an inside. pretty bad "detection" but
668         // works for now.
669         if (n.inside !is null) {
670             // must analyze the n node because it is the one holding the
671             // children which is the whole hierarchy of nodes. the inside is
672             // "just" the inside of the block to delete.
673             sdlBlock(n, ast.location(n.inside), stmtDelMutations(n.kind));
674         }
675 
676         accept(n, this);
677     }
678 
679     private void visitUnaryOp(T)(T n) {
680         auto loc = ast.location(n);
681 
682         Mutation.Kind[] expr;
683 
684         {
685             import dextool.plugin.mutate.backend.mutation_type.uoi;
686 
687             expr ~= uoiMutations(n.kind);
688         }
689         if (isDirectParent(Kind.Block)) {
690             expr ~= stmtDelMutations(n.kind);
691         }
692 
693         put(loc, expr, n.blacklist);
694     }
695 
696     private void visitComparisonBinaryOp(T)(T n) {
697         Mutation.Kind[] expr, op, lhs, rhs;
698 
699         {
700             import dextool.plugin.mutate.backend.mutation_type.ror;
701 
702             auto m = rorMutations(RorInfo(n.kind, ast.type(n.lhs),
703                     ast.symbol(n.lhs), ast.type(n.rhs), ast.symbol(n.rhs)));
704             expr ~= m.expr;
705             op ~= m.op;
706         }
707         {
708             import dextool.plugin.mutate.backend.mutation_type.lcr;
709 
710             auto m = lcrMutations(n.kind);
711             expr ~= m.expr;
712             op ~= m.op;
713             lhs ~= m.lhs;
714             rhs ~= m.rhs;
715         }
716         {
717             import dextool.plugin.mutate.backend.mutation_type.lcrb;
718 
719             auto m = lcrbMutations(n.kind);
720             op ~= m.op;
721             lhs ~= m.lhs;
722             rhs ~= m.rhs;
723         }
724         {
725             auto nty = ast.type(n);
726             expr ~= dcrMutations(DcrInfo(n.kind, ast.type(n)));
727         }
728         if (isDirectParent(Kind.Block)) {
729             expr ~= stmtDelMutations(n.kind);
730         }
731 
732         visitBinaryOp(n, op, lhs, rhs, expr);
733     }
734 
735     private void visitArithmeticBinaryOp(T)(T n) {
736         Mutation.Kind[] op, lhs, rhs, expr;
737 
738         {
739             import dextool.plugin.mutate.backend.mutation_type.aor;
740 
741             auto m = aorMutations(AorInfo(n.kind, ast.type(n.lhs), ast.type(n.rhs)));
742             op ~= m.op;
743             lhs ~= m.lhs;
744             rhs ~= m.rhs;
745         }
746         {
747             import dextool.plugin.mutate.backend.mutation_type.aors;
748 
749             op ~= aorsMutations(n.kind);
750         }
751         if (isDirectParent(Kind.Block)) {
752             expr ~= stmtDelMutations(n.kind);
753         }
754 
755         visitBinaryOp(n, op, lhs, rhs, expr);
756     }
757 
758     private void visitBinaryOp(T)(T n, Mutation.Kind[] op, Mutation.Kind[] lhs,
759             Mutation.Kind[] rhs, Mutation.Kind[] expr) {
760         auto locExpr = ast.location(n);
761         auto locOp = ast.location(n.operator);
762 
763         put(locExpr, expr, n.blacklist);
764         put(locOp, op, n.operator.blacklist);
765         // the interval check:
766         // != is sufficiently for unary operators such as ++.
767         // but there are also malformed that can be created thus '<' is needed.
768         // Change this to != and run on the game tutorial to see them being
769         // produced. Seems to be something with templates and function calls.
770         if (n.lhs !is null && locExpr.interval.begin < locOp.interval.end) {
771             auto offset = Interval(locExpr.interval.begin, locOp.interval.end);
772             put(Location(locOp.file, offset, SourceLocRange(locExpr.sloc.begin,
773                     locOp.sloc.end)), lhs, n.lhs.blacklist);
774         }
775         if (n.rhs !is null && locOp.interval.begin < locExpr.interval.end) {
776             auto offset = Interval(locOp.interval.begin, locExpr.interval.end);
777             put(Location(locOp.file, offset, SourceLocRange(locOp.sloc.begin,
778                     locExpr.sloc.end)), rhs, n.rhs.blacklist);
779         }
780     }
781 
782     private void sdlBlock(T)(T n, Location delLoc, Mutation.Kind[] op) @trusted {
783         scope sdlAnalyze = new DeleteBlockVisitor(ast);
784         sdlAnalyze.startVisit(n, delLoc);
785 
786         if (sdlAnalyze.canRemove)
787             put(delLoc, op, n.blacklist);
788     }
789 }
790 
791 /** Analyze a block to see if its content can be removed without introducing
792  * any undefined behavior.
793  *
794  * The block must:
795  *  * contain something.
796  *  * not contain a `Return` that returns a type other than void.
797  */
798 class DeleteBlockVisitor : DepthFirstVisitor {
799     Ast* ast;
800 
801     private {
802         bool hasReturn;
803         bool hasInnerNodes;
804     }
805 
806     alias visit = DepthFirstVisitor.visit;
807 
808     this(Ast* ast) {
809         this.ast = ast;
810     }
811 
812     // if the analyzer has determined that this node in the tree can be removed
813     // with SDL. Note though that it doesn't know anything about the parent
814     // node.
815     bool canRemove() {
816         return !hasReturn && hasInnerNodes;
817     }
818 
819     /// The node to start analysis from.
820     void startVisit(Node n, Location l) {
821         hasInnerNodes = !n.children.empty;
822 
823         if (hasInnerNodes && l.interval.begin < l.interval.end)
824             visit(n);
825     }
826 
827     override void visit(Return n) {
828         if (n.children.empty)
829             accept(n, this);
830         else
831             hasReturn = true;
832     }
833 }