1 /**
2 Copyright: Copyright (c) 2017, 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 This module contains functionality for reporting the mutations. Both the
11 summary and details for each mutation.
12 */
13 module dextool.plugin.mutate.backend.report;
14 
15 import std.exception : collectException;
16 import logger = std.experimental.logger;
17 
18 import dextool.type;
19 
20 import dextool.plugin.mutate.backend.database : Database, IterateMutantRow;
21 import dextool.plugin.mutate.backend.interface_ : FilesysIO, SafeInput;
22 import dextool.plugin.mutate.type : MutationKind, ReportKind, ReportLevel,
23     ReportSection;
24 import dextool.plugin.mutate.backend.type : Mutation, Offset;
25 
26 import dextool.plugin.mutate.backend.report.type : SimpleWriter, ReportEvent;
27 import dextool.plugin.mutate.backend.report.utility : MakeMutationTextResult,
28     window, windowSize, makeMutationText;
29 
30 ExitStatusType runReport(ref Database db, const MutationKind[] kind, const ReportKind report_kind,
31         const ReportLevel report_level, const ReportSection[] sections, FilesysIO fio) @safe nothrow {
32     import std.stdio : write;
33     import dextool.plugin.mutate.backend.utility;
34 
35     const auto kinds = dextool.plugin.mutate.backend.utility.toInternal(kind);
36 
37     try {
38         auto genrep = ReportGenerator.make(kind, report_kind, report_level, sections, fio);
39         genrep.mutationKindEvent(kind is null ? [MutationKind.any] : kind);
40 
41         genrep.locationStartEvent;
42         db.iterateMutants(kinds, &genrep.locationEvent);
43         genrep.locationEndEvent;
44 
45         genrep.locationStatEvent;
46 
47         genrep.statEvent(db);
48     }
49     catch (Exception e) {
50         logger.error(e.msg).collectException;
51     }
52 
53     return ExitStatusType.Ok;
54 }
55 
56 @safe:
57 private:
58 
59 /**
60  * Expects the event to come in the following order:
61  *  - mutationKindEvent
62  *  - locationStartEvent
63  *  - locationEvent
64  *  - locationEndEvent
65  *  - statStartEvent
66  *  - statEvent
67  *  - statEndEvent
68  */
69 struct ReportGenerator {
70     import std.algorithm : each;
71     import dextool.plugin.mutate.backend.report.markdown;
72     import dextool.plugin.mutate.backend.report.plain;
73     import dextool.plugin.mutate.backend.report.csv;
74 
75     ReportEvent[] listeners;
76 
77     static auto make(const MutationKind[] kind, const ReportKind report_kind,
78             const ReportLevel report_level, const ReportSection[] sections, FilesysIO fio) {
79         import dextool.plugin.mutate.backend.utility;
80 
81         auto kinds = dextool.plugin.mutate.backend.utility.toInternal(kind);
82         ReportEvent[] listeners;
83 
84         final switch (report_kind) {
85         case ReportKind.plain:
86             listeners = [new ReportPlain(kinds, report_level, sections, fio)];
87             break;
88         case ReportKind.markdown:
89             listeners = [new ReportMarkdown(kinds, report_level, fio)];
90             break;
91         case ReportKind.compiler:
92             listeners = [new ReportCompiler(kinds, report_level, fio)];
93             break;
94         case ReportKind.json:
95             listeners = [new ReportJson(report_level, fio)];
96             break;
97         case ReportKind.csv:
98             listeners = [new ReportCSV(kinds, report_level, fio)];
99             break;
100         }
101 
102         return ReportGenerator(listeners);
103     }
104 
105     void mutationKindEvent(const MutationKind[] kind_) {
106         listeners.each!(a => a.mutationKindEvent(kind_));
107     }
108 
109     void locationStartEvent() {
110         listeners.each!(a => a.locationStartEvent);
111     }
112 
113     // trusted: trusting that d2sqlite3 and sqlite3 is memory safe.
114     void locationEvent(const ref IterateMutantRow r) @trusted {
115         listeners.each!(a => a.locationEvent(r));
116     }
117 
118     void locationEndEvent() {
119         listeners.each!(a => a.locationEndEvent);
120     }
121 
122     void locationStatEvent() {
123         listeners.each!(a => a.locationStatEvent);
124     }
125 
126     void statEvent(ref Database db) {
127         listeners.each!(a => a.statEvent(db));
128     }
129 }
130 
131 struct CompilerConsole(Writer) {
132     import std.ascii : newline;
133     import std.format : formattedWrite, format;
134     import std.range : put;
135 
136     private Writer w;
137 
138     this(Writer w) {
139         this.w = w;
140     }
141 
142     auto build() {
143         return CompilerMsgBuilder!Writer(w);
144     }
145 }
146 
147 /** Build a compiler msg that follows how GCC would print to the console.
148  *
149  * Note that it should write to stderr.
150  */
151 struct CompilerMsgBuilder(Writer) {
152     import std.ascii : newline;
153     import std.format : formattedWrite;
154     import std.range : put;
155 
156     private {
157         Writer w;
158         string file_;
159         long line_;
160         long column_;
161     }
162 
163     this(Writer w) {
164         this.w = w;
165     }
166 
167     auto file(string a) {
168         file_ = a;
169         return this;
170     }
171 
172     auto line(long a) {
173         line_ = a;
174         return this;
175     }
176 
177     auto column(long a) {
178         column_ = a;
179         return this;
180     }
181 
182     auto begin(ARGS...)(auto ref ARGS args) {
183         // for now the severity is hard coded to warning because nothing else
184         // needs to be supported.
185         formattedWrite(w, "%s:%s:%s: warning: ", file_, line_, column_);
186         formattedWrite(w, args);
187         put(w, newline);
188         return this;
189     }
190 
191     auto note(ARGS...)(auto ref ARGS args) {
192         formattedWrite(w, "%s:%s:%s: note: ", file_, line_, column_);
193         formattedWrite(w, args);
194         put(w, newline);
195         return this;
196     }
197 
198     auto fixit(long offset, string mutation) {
199         // Example of a fixit hint from gcc:
200         // fix-it:"foo.cpp":{5:12-5:17}:"argc"
201         // the second value in the location is bytes (starting from 1) from the
202         // start of the line.
203         formattedWrite(w, `fix-it:"%s":{%s:%s-%s:%s}:"%s"`, file_, line_,
204                 column_, line_, column_ + offset, mutation);
205         put(w, newline);
206         return this;
207     }
208 
209     void end() {
210     }
211 }
212 
213 /** Report mutations as gcc would do for compilation warnings with fixit hints.
214  *
215  * #SPC-plugin_mutate_report_for_tool_integration
216  */
217 @safe final class ReportCompiler : ReportEvent {
218     import std.algorithm : each;
219     import std.conv : to;
220     import std.format : format;
221     import dextool.plugin.mutate.backend.utility;
222 
223     const Mutation.Kind[] kinds;
224     const ReportLevel report_level;
225     FilesysIO fio;
226 
227     CompilerConsole!SimpleWriter compiler;
228 
229     this(Mutation.Kind[] kinds, ReportLevel report_level, FilesysIO fio) {
230         this.kinds = kinds;
231         this.report_level = report_level;
232         this.fio = fio;
233     }
234 
235     override void mutationKindEvent(const MutationKind[]) {
236         compiler = CompilerConsole!SimpleWriter(delegate(const(char)[] s) @trusted{
237             import std.stdio : stderr, write;
238 
239             stderr.write(s);
240         });
241     }
242 
243     override void locationStartEvent() {
244     }
245 
246     override void locationEvent(const ref IterateMutantRow r) @trusted {
247         import dextool.plugin.mutate.backend.generate_mutant : makeMutation;
248 
249         void report() {
250             AbsolutePath abs_path;
251             MakeMutationTextResult mut_txt;
252             try {
253                 abs_path = AbsolutePath(FileName(r.file), DirName(fio.getOutputDir));
254                 mut_txt = makeMutationText(fio.makeInput(abs_path),
255                         r.mutationPoint.offset, r.mutation.kind);
256             }
257             catch (Exception e) {
258                 logger.warning(e.msg);
259             }
260 
261             // dfmt off
262             auto b = compiler.build
263                 .file(abs_path)
264                 .line(r.sloc.line)
265                 .column(r.sloc.column)
266                 .begin("%s: replace '%s' with '%s'", r.mutation.kind.toUser,
267                        window(mut_txt.original, windowSize),
268                        window(mut_txt.mutation, windowSize))
269                 .note("status:%s id:%s", r.mutation.status, r.id);
270 
271             if (mut_txt.original.length > windowSize)
272                 b = b.note("replace '%s'", mut_txt.original);
273             if (mut_txt.mutation.length > windowSize)
274                 b = b.note("with '%s'", mut_txt.mutation);
275 
276             b.fixit(r.mutationPoint.offset.end - r.mutationPoint.offset.begin, mut_txt.mutation)
277                 .end;
278             // dfmt on
279         }
280 
281         try {
282             // summary is the default and according to the specification of the
283             // default for tool integration alive mutations shall be printed.
284             final switch (report_level) {
285             case ReportLevel.summary:
286                 break;
287             case ReportLevel.alive:
288                 if (r.mutation.status == Mutation.Status.alive) {
289                     report();
290                 }
291                 break;
292             case ReportLevel.all:
293                 report();
294                 break;
295             }
296         }
297         catch (Exception e) {
298             logger.trace(e.msg).collectException;
299         }
300     }
301 
302     override void locationEndEvent() {
303     }
304 
305     override void locationStatEvent() {
306     }
307 
308     override void statEvent(ref Database db) {
309     }
310 }
311 
312 /**
313  * Expects locations to be grouped by file.
314  *
315  * TODO this is ugly. Use a JSON serializer instead.
316  */
317 @safe final class ReportJson : ReportEvent {
318     import std.array : array;
319     import std.algorithm : map, joiner;
320     import std.conv : to;
321     import std.format : format;
322     import std.json;
323 
324     const ReportLevel report_level;
325     FilesysIO fio;
326 
327     JSONValue report;
328     JSONValue current_file;
329 
330     Path last_file;
331 
332     this(ReportLevel report_level, FilesysIO fio) {
333         this.report_level = report_level;
334         this.fio = fio;
335     }
336 
337     override void mutationKindEvent(const MutationKind[] kinds) {
338         report = ["types" : kinds.map!(a => a.to!string).array, "files" : []];
339     }
340 
341     override void locationStartEvent() {
342     }
343 
344     override void locationEvent(const ref IterateMutantRow r) @trusted {
345         bool new_file;
346 
347         if (last_file.length == 0) {
348             current_file = ["filename" : r.file, "checksum" : format("%x", r.fileChecksum)];
349             new_file = true;
350         } else if (last_file != r.file) {
351             report["files"].array ~= current_file;
352             current_file = ["filename" : r.file, "checksum" : format("%x", r.fileChecksum)];
353             new_file = true;
354         }
355 
356         auto appendMutant() {
357             JSONValue m = ["id" : r.id.to!long];
358             m.object["kind"] = r.mutation.kind.to!string;
359             m.object["status"] = r.mutation.status.to!string;
360             m.object["line"] = r.sloc.line;
361             m.object["column"] = r.sloc.column;
362             m.object["begin"] = r.mutationPoint.offset.begin;
363             m.object["end"] = r.mutationPoint.offset.end;
364 
365             try {
366                 MakeMutationTextResult mut_txt;
367                 auto abs_path = AbsolutePath(FileName(r.file), DirName(fio.getOutputDir));
368                 mut_txt = makeMutationText(fio.makeInput(abs_path),
369                         r.mutationPoint.offset, r.mutation.kind);
370                 m.object["value"] = mut_txt.mutation;
371             }
372             catch (Exception e) {
373                 logger.warning(e.msg);
374             }
375             if (new_file) {
376                 last_file = r.file;
377                 current_file.object["mutants"] = JSONValue([m]);
378             } else {
379                 current_file["mutants"].array ~= m;
380             }
381         }
382 
383         final switch (report_level) {
384         case ReportLevel.summary:
385             break;
386         case ReportLevel.alive:
387             if (r.mutation.status == Mutation.Status.alive) {
388                 appendMutant;
389             }
390             break;
391         case ReportLevel.all:
392             appendMutant;
393             break;
394         }
395     }
396 
397     override void locationEndEvent() @trusted {
398         report["files"].array ~= current_file;
399     }
400 
401     override void locationStatEvent() {
402         import std.stdio : writeln;
403 
404         writeln(report.toJSON(true));
405     }
406 
407     override void statEvent(ref Database db) {
408     }
409 }