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 #SPC-analyzer
11 
12 TODO cache the checksums. They are *heavy*.
13 */
14 module dextool.plugin.mutate.backend.analyze;
15 
16 import logger = std.experimental.logger;
17 import std.algorithm : map, filter, joiner, cache;
18 import std.array : array, appender, empty;
19 import std.concurrency;
20 import std.datetime : dur;
21 import std.exception : collectException;
22 import std.parallelism;
23 import std.range : tee, enumerate;
24 import std.typecons;
25 
26 import colorlog;
27 
28 import dextool.compilation_db : CompileCommandFilter, defaultCompilerFlagFilter,
29     CompileCommandDB, SearchResult;
30 import dextool.plugin.mutate.backend.analyze.internal : Cache, TokenStream;
31 import dextool.plugin.mutate.backend.database : Database, LineMetadata, MutationPointEntry2;
32 import dextool.plugin.mutate.backend.database.type : MarkedMutant;
33 import dextool.plugin.mutate.backend.diff_parser : Diff;
34 import dextool.plugin.mutate.backend.interface_ : ValidateLoc, FilesysIO;
35 import dextool.plugin.mutate.backend.report.utility : statusToString, Table;
36 import dextool.plugin.mutate.backend.utility : checksum, trustedRelativePath,
37     Checksum, getProfileResult, Profile;
38 import dextool.plugin.mutate.config : ConfigCompiler, ConfigAnalyze;
39 import dextool.set;
40 import dextool.type : ExitStatusType, AbsolutePath, Path;
41 import dextool.user_filerange;
42 
43 version (unittest) {
44     import unit_threaded.assertions;
45 }
46 
47 /** Analyze the files in `frange` for mutations.
48  */
49 ExitStatusType runAnalyzer(ref Database db, ConfigAnalyze conf_analyze,
50         ConfigCompiler conf_compiler, UserFileRange frange, ValidateLoc val_loc, FilesysIO fio) @trusted {
51     import dextool.plugin.mutate.backend.diff_parser : diffFromStdin, Diff;
52 
53     auto fileFilter = () {
54         try {
55             return FileFilter(fio.getOutputDir, conf_analyze.unifiedDiffFromStdin,
56                     conf_analyze.unifiedDiffFromStdin ? diffFromStdin : Diff.init);
57         } catch (Exception e) {
58             logger.info(e.msg);
59             logger.warning("Unable to parse diff");
60         }
61         return FileFilter.init;
62     }();
63 
64     auto pool = () {
65         if (conf_analyze.poolSize == 0)
66             return new TaskPool();
67         return new TaskPool(conf_analyze.poolSize);
68     }();
69 
70     // will only be used by one thread at a time.
71     auto store = spawn(&storeActor, cast(shared)&db, cast(shared) fio.dup,
72             conf_analyze.prune, conf_analyze.fastDbStore,
73             conf_analyze.poolSize, conf_analyze.forceSaveAnalyze);
74 
75     int taskCnt;
76     Set!AbsolutePath alreadyAnalyzed;
77     // dfmt off
78     foreach (f; frange.filter!(a => !a.isNull)
79             .map!(a => a.get)
80             // The tool only supports analyzing a file one time.
81             // This optimize it in some cases where the same file occurs
82             // multiple times in the compile commands database.
83             .filter!(a => a.absoluteFile !in alreadyAnalyzed)
84             .tee!(a => alreadyAnalyzed.add(a.absoluteFile))
85             .cache
86             .filter!(a => !isPathInsideAnyRoot(conf_analyze.exclude, a.absoluteFile))
87             .filter!(a => fileFilter.shouldAnalyze(a.absoluteFile))) {
88         try {
89             pool.put(task!analyzeActor(f, val_loc.dup, fio.dup, conf_compiler, conf_analyze, store));
90             taskCnt++;
91         } catch (Exception e) {
92             logger.trace(e);
93             logger.warning(e.msg);
94         }
95     }
96     // dfmt on
97 
98     // inform the store actor of how many analyse results it should *try* to
99     // save.
100     send(store, AnalyzeCntMsg(taskCnt));
101     // wait for all files to be analyzed
102     pool.finish(true);
103     // wait for the store actor to finish
104     receiveOnly!StoreDoneMsg;
105 
106     if (conf_analyze.profile)
107         try {
108             import std.stdio : writeln;
109 
110             writeln(getProfileResult.toString);
111         } catch (Exception e) {
112             logger.warning("Unable to print the profile data: ", e.msg).collectException;
113         }
114 
115     return ExitStatusType.Ok;
116 }
117 
118 @safe:
119 
120 /** Filter function for files. Either all or those in stdin.
121  *
122  * The matching ignores the file extension in order to lessen the problem of a
123  * file that this approach skip headers because they do not exist in
124  * `compile_commands.json`. It means that e.g. "foo.hpp" would match `true` if
125  * `foo.cpp` is in `compile_commands.json`.
126  */
127 struct FileFilter {
128     import std.path : stripExtension;
129 
130     Set!string files;
131     bool useFileFilter;
132     AbsolutePath root;
133 
134     this(AbsolutePath root, bool fromStdin, Diff diff) {
135         this.root = root;
136         this.useFileFilter = fromStdin;
137         foreach (a; diff.toRange(root)) {
138             files.add(a.key.stripExtension);
139         }
140     }
141 
142     bool shouldAnalyze(AbsolutePath p) {
143         import std.path : relativePath;
144 
145         if (!useFileFilter) {
146             return true;
147         }
148 
149         return relativePath(p, root).stripExtension in files;
150     }
151 }
152 
153 /// Number of analyze tasks that has been spawned that the `storeActor` should wait for.
154 struct AnalyzeCntMsg {
155     int value;
156 }
157 
158 struct StoreDoneMsg {
159 }
160 
161 /// Start an analyze of a file
162 void analyzeActor(SearchResult fileToAnalyze, ValidateLoc vloc, FilesysIO fio,
163         ConfigCompiler compilerConf, ConfigAnalyze analyzeConf, Tid storeActor) @trusted nothrow {
164     auto profile = Profile("analyze file " ~ fileToAnalyze.absoluteFile);
165 
166     try {
167         auto analyzer = Analyze(vloc, fio,
168                 Analyze.Config(compilerConf.forceSystemIncludes, analyzeConf.mutantsPerSchema));
169         analyzer.process(fileToAnalyze);
170         send(storeActor, cast(immutable) analyzer.result);
171         return;
172     } catch (Exception e) {
173     }
174 
175     // send a dummy result
176     try {
177         send(storeActor, cast(immutable) new Analyze.Result);
178     } catch (Exception e) {
179     }
180 }
181 
182 /// Store the result of the analyze.
183 void storeActor(scope shared Database* dbShared, scope shared FilesysIO fioShared,
184         const bool prune, const bool fastDbStore, const long poolSize, const bool forceSave) @trusted nothrow {
185     import cachetools : CacheLRU;
186     import dextool.cachetools : nullableCache;
187     import dextool.plugin.mutate.backend.database : LineMetadata, FileId, LineAttr, NoMut;
188 
189     Database* db = cast(Database*) dbShared;
190     FilesysIO fio = cast(FilesysIO) fioShared;
191 
192     // A file is at most saved one time to the database.
193     Set!AbsolutePath savedFiles;
194 
195     auto getFileId = nullableCache!(string, FileId, (string p) => db.getFileId(p.Path))(256,
196             30.dur!"seconds");
197     auto getFileDbChecksum = nullableCache!(string, Checksum,
198             (string p) => db.getFileChecksum(p.Path))(256, 30.dur!"seconds");
199     auto getFileFsChecksum = nullableCache!(string, Checksum, (string p) {
200         return checksum(fio.makeInput(AbsolutePath(Path(p))).content[]);
201     })(256, 30.dur!"seconds");
202 
203     static struct Files {
204         Checksum[Path] value;
205 
206         this(ref Database db) {
207             foreach (a; db.getDetailedFiles) {
208                 value[a.file] = a.fileChecksum;
209             }
210         }
211     }
212 
213     void save(immutable Analyze.Result result) {
214         // mark files that have an unchanged checksum as "already saved"
215         foreach (f; result.idFile
216                 .byKey
217                 .filter!(a => a !in savedFiles)
218                 .filter!(a => getFileDbChecksum(fio.toRelativeRoot(a)) == getFileFsChecksum(a)
219                     && !forceSave)) {
220             logger.info("Unchanged ".color(Color.yellow), f);
221             savedFiles.add(f);
222         }
223 
224         // only saves mutation points to a file one time.
225         {
226             auto app = appender!(MutationPointEntry2[])();
227             foreach (mp; result.mutationPoints
228                     .map!(a => tuple!("data", "file")(a, fio.toAbsoluteRoot(a.file)))
229                     .filter!(a => a.file !in savedFiles)) {
230                 app.put(mp.data);
231             }
232             foreach (f; result.idFile.byKey.filter!(a => a !in savedFiles)) {
233                 logger.info("Saving ".color(Color.green), f);
234                 const relp = fio.toRelativeRoot(f);
235                 db.removeFile(relp);
236                 const info = result.infoId[result.idFile[f]];
237                 db.put(relp, info.checksum, info.language);
238                 savedFiles.add(f);
239             }
240             db.put(app.data, fio.getOutputDir);
241         }
242 
243         foreach (s; result.schematas.enumerate) {
244             try {
245                 auto mutants = result.schemataMutants[s.index].map!(
246                         a => db.getMutationStatusId(a.value))
247                     .filter!(a => !a.isNull)
248                     .map!(a => a.get)
249                     .array;
250                 if (!mutants.empty && !s.value.empty) {
251                     const id = db.putSchemata(result.schemataChecksum[s.index], s.value, mutants);
252                     logger.trace(!id.isNull, "Saving schemata ", id.get.value);
253                 }
254             } catch (Exception e) {
255                 logger.trace(e.msg);
256                 logger.warning("Unable to save schemata ", s.index).collectException;
257             }
258         }
259 
260         {
261             Set!long printed;
262             auto app = appender!(LineMetadata[])();
263             foreach (md; result.metadata) {
264                 // transform the ID from local to global.
265                 const fid = getFileId(fio.toRelativeRoot(result.fileId[md.id]));
266                 if (fid.isNull && !printed.contains(md.id)) {
267                     printed.add(md.id);
268                     logger.warningf("File with suppressed mutants (// NOMUT) not in the database: %s. Skipping...",
269                             result.fileId[md.id]).collectException;
270                 } else if (!fid.isNull) {
271                     app.put(LineMetadata(fid.get, md.line, md.attr));
272                 }
273             }
274             db.put(app.data);
275         }
276     }
277 
278     // listen for results from workers until the expected number is processed.
279     void recv() {
280         auto profile = Profile("updating files");
281         logger.info("Updating files");
282 
283         int resultCnt;
284         Nullable!int maxResults;
285         bool running = true;
286 
287         while (running) {
288             try {
289                 receive((AnalyzeCntMsg a) { maxResults = a.value; }, (immutable Analyze.Result a) {
290                     resultCnt++;
291                     save(a);
292                 },);
293             } catch (Exception e) {
294                 logger.trace(e).collectException;
295                 logger.warning(e.msg).collectException;
296             }
297 
298             if (!maxResults.isNull && resultCnt >= maxResults.get) {
299                 running = false;
300             }
301         }
302     }
303 
304     void pruneFiles() {
305         import std.path : buildPath;
306 
307         auto profile = Profile("prune files");
308 
309         logger.info("Pruning the database of dropped files");
310         auto files = db.getFiles.map!(a => fio.toAbsoluteRoot(a)).toSet;
311 
312         foreach (f; files.setDifference(savedFiles).toRange) {
313             logger.info("Removing ".color(Color.red), f);
314             db.removeFile(fio.toRelativeRoot(f));
315         }
316     }
317 
318     void fastDbOn() {
319         if (!fastDbStore)
320             return;
321         logger.info(
322                 "Turning OFF sqlite3 synchronization protection to improve the write performance");
323         logger.warning("Do NOT interrupt dextool in any way because it may corrupt the database");
324         db.run("PRAGMA synchronous = OFF");
325         db.run("PRAGMA journal_mode = MEMORY");
326     }
327 
328     void fastDbOff() {
329         if (!fastDbStore)
330             return;
331         db.run("PRAGMA synchronous = ON");
332         db.run("PRAGMA journal_mode = DELETE");
333     }
334 
335     try {
336         import dextool.plugin.mutate.backend.test_mutant.timeout : resetTimeoutContext;
337 
338         // by making the mailbox size follow the number of workers the overall
339         // behavior will slow down if saving to the database is too slow. This
340         // avoids excessive or even fatal memory usage.
341         setMaxMailboxSize(thisTid, poolSize + 2, OnCrowding.block);
342 
343         fastDbOn();
344 
345         auto trans = db.transaction;
346 
347         // TODO: only remove those files that are modified.
348         logger.info("Removing metadata");
349         db.clearMetadata;
350 
351         recv();
352 
353         // TODO: print what files has been updated.
354         logger.info("Resetting timeout context");
355         resetTimeoutContext(*db);
356 
357         logger.info("Updating metadata");
358         db.updateMetadata;
359 
360         if (prune) {
361             pruneFiles();
362             {
363                 auto profile = Profile("remove orphaned mutants");
364                 logger.info("Removing orphaned mutants");
365                 db.removeOrphanedMutants;
366             }
367             {
368                 auto profile = Profile("prune schematas");
369                 logger.info("Prune schematas");
370                 db.pruneSchemas;
371             }
372         }
373 
374         logger.info("Updating manually marked mutants");
375         updateMarkedMutants(*db);
376         printLostMarkings(db.getLostMarkings);
377 
378         logger.info("Committing changes");
379         trans.commit;
380         logger.info("Ok".color(Color.green));
381 
382         fastDbOff();
383     } catch (Exception e) {
384         logger.error(e.msg).collectException;
385         logger.error("Failed to save the result of the analyze to the database").collectException;
386     }
387 
388     try {
389         send(ownerTid, StoreDoneMsg.init);
390     } catch (Exception e) {
391         logger.errorf("Fatal error. Unable to send %s to the main thread",
392                 StoreDoneMsg.init).collectException;
393     }
394 }
395 
396 /// Analyze a file for mutants.
397 struct Analyze {
398     import std.regex : Regex, regex, matchFirst;
399     import std.typecons : Yes;
400     import cpptooling.analyzer.clang.context : ClangContext;
401     import cpptooling.utility.virtualfilesystem;
402     import dextool.compilation_db : SearchResult;
403     import dextool.type : Exists, makeExists;
404 
405     static struct Config {
406         bool forceSystemIncludes;
407         long mutantsPerSchema;
408     }
409 
410     private {
411         static immutable raw_re_nomut = `^((//)|(/\*))\s*NOMUT\s*(\((?P<tag>.*)\))?\s*((?P<comment>.*)\*/|(?P<comment>.*))?`;
412 
413         Regex!char re_nomut;
414 
415         ValidateLoc val_loc;
416         FilesysIO fio;
417         bool forceSystemIncludes;
418 
419         Cache cache;
420 
421         Result result;
422 
423         Config conf;
424     }
425 
426     this(ValidateLoc val_loc, FilesysIO fio, Config conf) @trusted {
427         this.val_loc = val_loc;
428         this.fio = fio;
429         this.cache = new Cache;
430         this.re_nomut = regex(raw_re_nomut);
431         this.forceSystemIncludes = forceSystemIncludes;
432         this.result = new Result;
433         this.conf = conf;
434     }
435 
436     void process(SearchResult in_file) @safe {
437         in_file.flags.forceSystemIncludes = conf.forceSystemIncludes;
438 
439         // find the file and flags to analyze
440         Exists!AbsolutePath checked_in_file;
441         try {
442             checked_in_file = makeExists(in_file.absoluteFile);
443         } catch (Exception e) {
444             logger.warning(e.msg);
445             return;
446         }
447 
448         try {
449             () @trusted {
450                 auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
451                 auto tstream = new TokenStreamImpl(ctx);
452 
453                 analyzeForMutants(in_file, checked_in_file, ctx, tstream);
454                 // TODO: filter files so they are only analyzed once for comments
455                 foreach (f; result.fileId.byValue)
456                     analyzeForComments(f, tstream);
457             }();
458         } catch (Exception e) {
459             () @trusted { logger.trace(e); }();
460             logger.info(e.msg);
461             logger.error("failed analyze of ", in_file).collectException;
462         }
463     }
464 
465     void analyzeForMutants(SearchResult in_file,
466             Exists!AbsolutePath checked_in_file, ref ClangContext ctx, TokenStream tstream) @safe {
467         import dextool.plugin.mutate.backend.analyze.pass_clang;
468         import dextool.plugin.mutate.backend.analyze.pass_mutant;
469         import dextool.plugin.mutate.backend.analyze.pass_schemata;
470         import cpptooling.analyzer.clang.check_parse_result : hasParseErrors, logDiagnostic;
471 
472         logger.infof("Analyzing %s", checked_in_file);
473         auto tu = ctx.makeTranslationUnit(checked_in_file, in_file.flags.completeFlags);
474         if (tu.hasParseErrors) {
475             logDiagnostic(tu);
476             logger.errorf("Compile error in %s. Skipping", checked_in_file);
477             return;
478         }
479 
480         auto ast = toMutateAst(tu.cursor, fio);
481         debug logger.trace(ast);
482         auto mutants = toMutants(ast, fio, val_loc);
483 
484         debug logger.trace(mutants);
485         auto codeMutants = toCodeMutants(mutants, fio, tstream);
486         debug logger.trace(codeMutants);
487         () @trusted { .destroy(mutants); }();
488 
489         auto schemas = toSchemata(ast, fio, codeMutants, conf.mutantsPerSchema);
490         debug logger.trace(schemas);
491         foreach (f; schemas.getSchematas.filter!(a => !(a.fragments.empty || a.mutants.empty))) {
492             const id = result.schematas.length;
493             result.schematas ~= f.fragments;
494             result.schemataMutants[id] = f.mutants.map!(a => a.id).array;
495             result.schemataChecksum[id] = f.checksum;
496         }
497         () @trusted { .destroy(schemas); }();
498 
499         .destroy(ast);
500 
501         result.mutationPoints = codeMutants.points.byKeyValue.map!(
502                 a => a.value.map!(b => MutationPointEntry2(fio.toRelativeRoot(a.key),
503                 b.offset, b.sloc.begin, b.sloc.end, b.mutants))).joiner.array;
504         foreach (f; codeMutants.points.byKey) {
505             const id = result.idFile.length;
506             result.idFile[f] = id;
507             result.fileId[id] = f;
508             result.infoId[id] = Result.FileInfo(codeMutants.csFiles[f], codeMutants.lang);
509         }
510 
511         () @trusted { .destroy(codeMutants); .destroy(schemas); }();
512     }
513 
514     /** Tokens are always from the same file.
515      *
516      * TODO: move this to pass_clang.
517      */
518     void analyzeForComments(AbsolutePath file, TokenStream tstream) @trusted {
519         import std.algorithm : filter;
520         import clang.c.Index : CXTokenKind;
521         import dextool.plugin.mutate.backend.database : LineMetadata, FileId, LineAttr, NoMut;
522 
523         const fid = result.idFile.require(file, result.fileId.length).FileId;
524 
525         auto mdata = appender!(LineMetadata[])();
526         foreach (t; cache.getTokens(AbsolutePath(file), tstream)
527                 .filter!(a => a.kind == CXTokenKind.comment)) {
528             auto m = matchFirst(t.spelling, re_nomut);
529             if (m.whichPattern == 0)
530                 continue;
531 
532             mdata.put(LineMetadata(fid, t.loc.line, LineAttr(NoMut(m["tag"], m["comment"]))));
533             logger.tracef("NOMUT found at %s:%s:%s", file, t.loc.line, t.loc.column);
534         }
535 
536         result.metadata ~= mdata.data;
537     }
538 
539     static class Result {
540         import dextool.plugin.mutate.backend.type : Language, CodeChecksum, SchemataChecksum;
541         import dextool.plugin.mutate.backend.database.type : SchemataFragment;
542 
543         MutationPointEntry2[] mutationPoints;
544 
545         static struct FileInfo {
546             Checksum checksum;
547             Language language;
548         }
549 
550         /// The key is the ID from idFile.
551         FileInfo[ulong] infoId;
552 
553         /// The IDs is unique for *this* analyze, not globally.
554         long[AbsolutePath] idFile;
555         AbsolutePath[long] fileId;
556 
557         // The FileID used in the metadata is local to this analysis. It has to
558         // be remapped when added to the database.
559         LineMetadata[] metadata;
560 
561         /// Mutant schematas that has been generated.
562         SchemataFragment[][] schematas;
563         /// the mutants that are associated with a schemata.
564         CodeChecksum[][long] schemataMutants;
565         /// checksum for the schemata
566         SchemataChecksum[long] schemataChecksum;
567     }
568 }
569 
570 @(
571         "shall extract the tag and comment from the input following the pattern NOMUT with optional tag and comment")
572 unittest {
573     import std.regex : regex, matchFirst;
574     import unit_threaded.runner.io : writelnUt;
575 
576     auto re_nomut = regex(Analyze.raw_re_nomut);
577     // NOMUT in other type of comments should NOT match.
578     matchFirst("/// NOMUT", re_nomut).whichPattern.shouldEqual(0);
579     matchFirst("// stuff with NOMUT in it", re_nomut).whichPattern.shouldEqual(0);
580     matchFirst("/** NOMUT*/", re_nomut).whichPattern.shouldEqual(0);
581     matchFirst("/* stuff with NOMUT in it */", re_nomut).whichPattern.shouldEqual(0);
582 
583     matchFirst("/*NOMUT*/", re_nomut).whichPattern.shouldEqual(1);
584     matchFirst("/*NOMUT*/", re_nomut)["comment"].shouldEqual("");
585     matchFirst("//NOMUT", re_nomut).whichPattern.shouldEqual(1);
586     matchFirst("// NOMUT", re_nomut).whichPattern.shouldEqual(1);
587     matchFirst("// NOMUT (arch)", re_nomut)["tag"].shouldEqual("arch");
588     matchFirst("// NOMUT smurf", re_nomut)["comment"].shouldEqual("smurf");
589     auto m = matchFirst("// NOMUT (arch) smurf", re_nomut);
590     m["tag"].shouldEqual("arch");
591     m["comment"].shouldEqual("smurf");
592 }
593 
594 /// Stream of tokens excluding comment tokens.
595 class TokenStreamImpl : TokenStream {
596     import cpptooling.analyzer.clang.context : ClangContext;
597     import dextool.plugin.mutate.backend.type : Token;
598     import dextool.plugin.mutate.backend.utility : tokenize;
599 
600     ClangContext* ctx;
601 
602     /// The context must outlive any instance of this class.
603     // TODO remove @trusted when upgrading to dmd-fe 2.091.0+ and activate dip25 + 1000
604     this(ref ClangContext ctx) @trusted {
605         this.ctx = &ctx;
606     }
607 
608     Token[] getTokens(Path p) {
609         return tokenize(*ctx, p);
610     }
611 
612     Token[] getFilteredTokens(Path p) {
613         import clang.c.Index : CXTokenKind;
614 
615         // Filter a stream of tokens for those that should affect the checksum.
616         return tokenize(*ctx, p).filter!(a => a.kind != CXTokenKind.comment).array;
617     }
618 }
619 
620 /// Returns: true if `f` is inside any `roots`.
621 bool isPathInsideAnyRoot(AbsolutePath[] roots, AbsolutePath f) @safe {
622     import dextool.utility : isPathInsideRoot;
623 
624     foreach (root; roots) {
625         if (isPathInsideRoot(root, f))
626             return true;
627     }
628 
629     return false;
630 }
631 
632 /** Update the connection between the marked mutants and their mutation status
633  * id and mutation id.
634  */
635 void updateMarkedMutants(ref Database db) {
636     import dextool.plugin.mutate.backend.database.type : MutationStatusId;
637 
638     void update(MarkedMutant m) {
639         const stId = db.getMutationStatusId(m.statusChecksum);
640         if (stId.isNull)
641             return;
642         const mutId = db.getMutationId(stId.get);
643         if (mutId.isNull)
644             return;
645         db.removeMarkedMutant(m.statusChecksum);
646         db.markMutant(mutId.get, m.path, m.sloc, stId.get, m.statusChecksum,
647                 m.toStatus, m.rationale, m.mutText);
648         db.updateMutationStatus(stId.get, m.toStatus);
649     }
650 
651     // find those marked mutants that have a checksum that is different from
652     // the mutation status the marked mutant is related to. If possible change
653     // the relation to the correct mutation status id.
654     foreach (m; db.getMarkedMutants
655             .map!(a => tuple(a, db.getChecksum(a.statusId)))
656             .filter!(a => !a[1].isNull)
657             .filter!(a => a[0].statusChecksum != a[1].get)) {
658         update(m[0]);
659     }
660 }
661 
662 /// Prints a marked mutant that has become lost due to rerun of analyze
663 void printLostMarkings(MarkedMutant[] lostMutants) {
664     import std.algorithm : sort;
665     import std.array : empty;
666     import std.conv : to;
667     import std.stdio : writeln;
668 
669     if (lostMutants.empty)
670         return;
671 
672     Table!6 tbl = Table!6([
673             "ID", "File", "Line", "Column", "Status", "Rationale"
674             ]);
675     foreach (m; lostMutants) {
676         typeof(tbl).Row r = [
677             m.mutationId.to!string, m.path, m.sloc.line.to!string,
678             m.sloc.column.to!string, m.toStatus.to!string, m.rationale
679         ];
680         tbl.put(r);
681     }
682     logger.warning("Marked mutants was lost");
683     writeln(tbl);
684 }
685 
686 @("shall only let files in the diff through")
687 unittest {
688     import std.string : lineSplitter;
689     import dextool.plugin.mutate.backend.diff_parser;
690 
691     immutable lines = `diff --git a/standalone2.d b/standalone2.d
692 index 0123..2345 100644
693 --- a/standalone.d
694 +++ b/standalone2.d
695 @@ -31,7 +31,6 @@ import std.algorithm : map;
696  import std.array : Appender, appender, array;
697  import std.datetime : SysTime;
698 +import std.format : format;
699 -import std.typecons : Tuple;
700 
701  import d2sqlite3 : sqlDatabase = Database;
702 
703 @@ -46,7 +45,7 @@ import dextool.plugin.mutate.backend.type : Language;
704  struct Database {
705      import std.conv : to;
706      import std.exception : collectException;
707 -    import std.typecons : Nullable;
708 +    import std.typecons : Nullable, Flag, No;
709      import dextool.plugin.mutate.backend.type : MutationPoint, Mutation, Checksum;
710 
711 +    sqlDatabase db;`;
712 
713     UnifiedDiffParser p;
714     foreach (line; lines.lineSplitter)
715         p.process(line);
716     auto diff = p.result;
717 
718     auto files = FileFilter(".".Path.AbsolutePath, true, diff);
719 
720     files.shouldAnalyze("standalone.d".Path.AbsolutePath).shouldBeFalse;
721     files.shouldAnalyze("standalone2.d".Path.AbsolutePath).shouldBeTrue;
722 }