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