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 Filter mutants based on simple textual pattern matching. These are the obvious
11 equivalent or undesired mutants.
12 */
13 module dextool.plugin.mutate.backend.analyze.pass_filter;
14 
15 import logger = std.experimental.logger;
16 import std.algorithm : among, map, filter, cache;
17 import std.array : appender, empty;
18 import std.typecons : Tuple;
19 
20 import blob_model : Blob;
21 
22 static import colorlog;
23 
24 import dextool.plugin.mutate.backend.interface_ : FilesysIO;
25 import dextool.plugin.mutate.backend.type : Language, Offset, Mutation;
26 import dextool.plugin.mutate.backend.analyze.pass_mutant : MutantsResult;
27 import dextool.plugin.mutate.backend.generate_mutant : makeMutationText, MakeMutationTextResult;
28 
29 alias log = colorlog.log!"analyze.pass_filter";
30 
31 shared static this() {
32     colorlog.make!(colorlog.SimpleLogger)(logger.LogLevel.info, "analyze.pass_filter");
33 }
34 
35 @safe:
36 
37 MutantsResult filterMutants(FilesysIO fio, MutantsResult mutants) {
38     foreach (f; mutants.files.map!(a => a.path)) {
39         log.trace(f);
40         auto file = fio.makeInput(f);
41         foreach (r; mutants.getMutationPoints(f)
42                 .map!(a => analyzeForUndesiredMutant(file, a, mutants.lang))
43                 .cache
44                 .filter!(a => !a.kind.empty)) {
45             foreach (k; r.kind) {
46                 mutants.drop(f, r.point, k);
47             }
48         }
49     }
50 
51     return mutants;
52 }
53 
54 private:
55 
56 alias Mutants = Tuple!(Mutation.Kind[], "kind", MutantsResult.MutationPoint, "point");
57 
58 /// Returns: mutants to drop from the mutation point.
59 Mutants analyzeForUndesiredMutant(Blob file, Mutants mutants, const Language lang) {
60     auto app = appender!(Mutation.Kind[])();
61 
62     foreach (k; mutants.kind) {
63         if (isEmpty(file, mutants.point.offset)) {
64             log.tracef("Dropping undesired mutant. Mutant is empty (%s %s %s)",
65                     file.uri, mutants.point, k);
66             app.put(k);
67             continue;
68         }
69 
70         auto mutant = makeMutationText(file, mutants.point.offset, k, lang);
71         if (isTextuallyEqual(file, mutants.point.offset, mutant.rawMutation)) {
72             log.tracef("Dropping undesired mutant. Original and mutant is textually equivalent (%s %s %s)",
73                     file.uri, mutants.point, k);
74             app.put(k);
75         } else if (lang.among(Language.assumeCpp, Language.cpp)
76                 && isUndesiredCppPattern(file, mutants.point.offset, mutant.rawMutation)) {
77             log.tracef("Dropping undesired mutant. The mutant is an undesired C++ mutant pattern (%s %s %s)",
78                     file.uri, mutants.point, k);
79             app.put(k);
80         } else if (isOnlyWhitespace(file, mutants.point.offset, mutant.rawMutation)) {
81             log.tracef("Dropping undesired mutant. Both the original and the mutant is only whitespaces (%s %s %s)",
82                     file.uri, mutants.point, k);
83             app.put(k);
84         }
85     }
86 
87     return Mutants(app.data, mutants.point);
88 }
89 
90 bool isEmpty(Blob file, Offset o) {
91     // well an empty region can just be removed
92     return o.isZero || o.end > file.content.length;
93 }
94 
95 bool isTextuallyEqual(Blob file, Offset o, const(ubyte)[] mutant) {
96     return file.content[o.begin .. o.end] == mutant;
97 }
98 
99 // if both the original and mutation is only whitespace
100 bool isOnlyWhitespace(Blob file, Offset o, const(ubyte)[] mutant) {
101     import std.algorithm : canFind;
102 
103     static immutable ubyte[6] whitespace = [
104         cast(ubyte) ' ', cast(ubyte) '\t', cast(ubyte) '\v', cast(ubyte) '\r',
105         cast(ubyte) '\n', cast(ubyte) '\f'
106     ];
107 
108     bool rval = true;
109     foreach (a; file.content[o.begin .. o.end]) {
110         rval = rval && whitespace[].canFind(a);
111     }
112 
113     foreach (a; mutant) {
114         rval = rval && whitespace[].canFind(a);
115     }
116 
117     return rval;
118 }
119 
120 bool isUndesiredCppPattern(Blob file, Offset o, const(ubyte)[] mutant) {
121     static immutable ubyte[2] ctorParenthesis = ['(', ')'];
122     static immutable ubyte[2] ctorCurly = ['{', '}'];
123     static immutable ubyte zero = '0';
124     static immutable ubyte one = '1';
125     static immutable ubyte[5] false_ = ['f', 'a', 'l', 's', 'e'];
126     static immutable ubyte[4] true_ = ['t', 'r', 'u', 'e'];
127 
128     // e.g. delete of the constructor {} is undesired. It is almost always an
129     // equivalent mutant.
130     if (o.end - o.begin == 2 && file.content[o.begin .. o.end].among(ctorParenthesis[],
131             ctorCurly[])) {
132         return true;
133     }
134 
135     // replacing '0' with 'false' and '1' with 'true' is equivalent
136     if (file.content[o.begin] == zero && false_ == mutant
137             || file.content[o.begin] == one && true_ == mutant) {
138         return true;
139     }
140 
141     return false;
142 }