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