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 
18 import dextool.compilation_db : CompileCommandFilter, defaultCompilerFlagFilter, CompileCommandDB;
19 import dextool.set;
20 import dextool.type : ExitStatusType, AbsolutePath, Path, DirName;
21 import dextool.user_filerange;
22 
23 import dextool.plugin.mutate.backend.analyze.internal : Cache, TokenStream;
24 import dextool.plugin.mutate.backend.analyze.visitor : makeRootVisitor;
25 import dextool.plugin.mutate.backend.database : Database;
26 import dextool.plugin.mutate.backend.interface_ : ValidateLoc, FilesysIO;
27 import dextool.plugin.mutate.backend.utility : checksum, trustedRelativePath, Checksum;
28 import dextool.plugin.mutate.config : ConfigCompiler;
29 
30 version (unittest) {
31     import unit_threaded.assertions;
32 }
33 
34 /** Analyze the files in `frange` for mutations.
35  */
36 ExitStatusType runAnalyzer(ref Database db, ConfigCompiler conf,
37         ref UserFileRange frange, ValidateLoc val_loc, FilesysIO fio) @safe {
38     auto analyzer = Analyzer(db, val_loc, fio, conf);
39 
40     foreach (in_file; frange) {
41         try {
42             analyzer.process(in_file);
43         } catch (Exception e) {
44             () @trusted { logger.trace(e); logger.warning(e.msg); }();
45         }
46     }
47     analyzer.finalize;
48 
49     return ExitStatusType.Ok;
50 }
51 
52 private:
53 
54 struct Analyzer {
55     import std.regex : Regex, regex, matchFirst;
56     import std.typecons : NullableRef, Nullable, Yes;
57     import cpptooling.analyzer.clang.context : ClangContext;
58     import cpptooling.utility.virtualfilesystem;
59     import dextool.compilation_db : SearchResult;
60     import dextool.type : FileName, Exists, makeExists;
61     import dextool.utility : analyzeFile;
62 
63     private {
64         static immutable raw_re_nomut = `^((//)|(/\*))\s*NOMUT\s*(\((?P<tag>.*)\))?\s*((?P<comment>.*)\*/|(?P<comment>.*))?`;
65 
66         // they are not by necessity the same.
67         // Input could be a file that is excluded via --restrict but pull in a
68         // header-only library that is allowed to be mutated.
69         Set!AbsolutePath analyzed_files;
70         Set!AbsolutePath files_with_mutations;
71 
72         Set!Path before_files;
73 
74         NullableRef!Database db;
75 
76         ValidateLoc val_loc;
77         FilesysIO fio;
78         ConfigCompiler conf;
79 
80         Cache cache;
81 
82         Regex!char re_nomut;
83     }
84 
85     this(ref Database db, ValidateLoc val_loc, FilesysIO fio, ConfigCompiler conf) @trusted {
86         this.db = &db;
87         this.before_files = db.getFiles.setFromList;
88         this.val_loc = val_loc;
89         this.fio = fio;
90         this.conf = conf;
91         this.cache = new Cache;
92         this.re_nomut = regex(raw_re_nomut);
93 
94         db.removeAllFiles;
95     }
96 
97     void process(Nullable!SearchResult in_file) @safe {
98         if (in_file.isNull)
99             return;
100 
101         // TODO: this should be generic for Dextool.
102         in_file.flags.forceSystemIncludes = conf.forceSystemIncludes;
103 
104         // find the file and flags to analyze
105         Exists!AbsolutePath checked_in_file;
106         try {
107             checked_in_file = makeExists(in_file.absoluteFile);
108         } catch (Exception e) {
109             logger.warning(e.msg);
110             return;
111         }
112 
113         if (analyzed_files.contains(checked_in_file))
114             return;
115 
116         analyzed_files.add(checked_in_file);
117 
118         () @trusted {
119             auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
120             auto tstream = new TokenStreamImpl(ctx);
121 
122             auto files = analyzeForMutants(in_file, checked_in_file, ctx, tstream);
123             // TODO: filter files so they are only analyzed once for comments
124             foreach (f; files)
125                 analyzeForComments(f, tstream);
126         }();
127     }
128 
129     Path[] analyzeForMutants(SearchResult in_file,
130             Exists!AbsolutePath checked_in_file, ref ClangContext ctx, TokenStream tstream) @safe {
131         import std.algorithm : map;
132         import std.array : array;
133 
134         auto root = makeRootVisitor(fio, val_loc, tstream, cache);
135         analyzeFile(checked_in_file, in_file.flags.completeFlags, root.visitor, ctx);
136 
137         foreach (a; root.mutationPointFiles) {
138             auto abs_path = AbsolutePath(a.path.FileName);
139             analyzed_files.add(abs_path);
140             files_with_mutations.add(abs_path);
141 
142             auto relp = trustedRelativePath(a.path.FileName, fio.getOutputDir);
143 
144             try {
145                 auto f_status = isFileChanged(db, relp, a.cs);
146                 if (f_status == FileStatus.changed) {
147                     logger.infof("Updating analyze of '%s'", a);
148                 }
149 
150                 db.put(Path(relp), a.cs, a.lang);
151             } catch (Exception e) {
152                 logger.warning(e.msg);
153             }
154         }
155 
156         db.put(root.mutationPoints, fio.getOutputDir);
157         return root.mutationPointFiles.map!(a => a.path).array;
158     }
159 
160     /**
161      * Tokens are always from the same file.
162      */
163     void analyzeForComments(Path file, TokenStream tstream) @trusted {
164         import std.algorithm : filter, countUntil, among, startsWith;
165         import std.array : appender;
166         import std.string : stripLeft;
167         import std.utf : byCodeUnit;
168         import clang.c.Index : CXTokenKind;
169         import dextool.plugin.mutate.backend.database : LineMetadata, FileId, LineAttr, NoMut;
170 
171         const fid = db.getFileId(fio.toRelativeRoot(file));
172         if (fid.isNull) {
173             logger.warningf("File with suppressed mutants (// NOMUT) not in the DB: %s. Skipping...",
174                     file);
175             return;
176         }
177 
178         auto mdata = appender!(LineMetadata[])();
179         foreach (t; cache.getTokens(AbsolutePath(file), tstream)
180                 .filter!(a => a.kind == CXTokenKind.comment)) {
181             auto m = matchFirst(t.spelling, re_nomut);
182             if (m.whichPattern == 0)
183                 continue;
184 
185             mdata.put(LineMetadata(fid, t.loc.line, LineAttr(NoMut(m["tag"], m["comment"]))));
186             logger.tracef("NOMUT found at %s:%s:%s", file, t.loc.line, t.loc.column);
187         }
188 
189         db.put(mdata.data);
190     }
191 
192     void finalize() @safe {
193         db.removeOrphanedMutants;
194         printPrunedFiles(before_files, files_with_mutations, fio.getOutputDir);
195     }
196 }
197 
198 @(
199         "shall extract the tag and comment from the input following the pattern NOMUT with optional tag and comment")
200 unittest {
201     import std.regex : regex, matchFirst;
202     import unit_threaded.runner.io : writelnUt;
203 
204     auto re_nomut = regex(Analyzer.raw_re_nomut);
205     // NOMUT in other type of comments should NOT match.
206     matchFirst("/// NOMUT", re_nomut).whichPattern.shouldEqual(0);
207     matchFirst("// stuff with NOMUT in it", re_nomut).whichPattern.shouldEqual(0);
208     matchFirst("/** NOMUT*/", re_nomut).whichPattern.shouldEqual(0);
209     matchFirst("/* stuff with NOMUT in it */", re_nomut).whichPattern.shouldEqual(0);
210 
211     matchFirst("/*NOMUT*/", re_nomut).whichPattern.shouldEqual(1);
212     matchFirst("/*NOMUT*/", re_nomut)["comment"].shouldEqual("");
213     matchFirst("//NOMUT", re_nomut).whichPattern.shouldEqual(1);
214     matchFirst("// NOMUT", re_nomut).whichPattern.shouldEqual(1);
215     matchFirst("// NOMUT (arch)", re_nomut)["tag"].shouldEqual("arch");
216     matchFirst("// NOMUT smurf", re_nomut)["comment"].shouldEqual("smurf");
217     auto m = matchFirst("// NOMUT (arch) smurf", re_nomut);
218     m["tag"].shouldEqual("arch");
219     m["comment"].shouldEqual("smurf");
220 }
221 
222 /// Stream of tokens excluding comment tokens.
223 class TokenStreamImpl : TokenStream {
224     import std.typecons : NullableRef, nullableRef;
225     import cpptooling.analyzer.clang.context : ClangContext;
226     import dextool.plugin.mutate.backend.type : Token;
227 
228     NullableRef!ClangContext ctx;
229 
230     /// The context must outlive any instance of this class.
231     this(ref ClangContext ctx) {
232         this.ctx = nullableRef(&ctx);
233     }
234 
235     Token[] getTokens(Path p) {
236         import dextool.plugin.mutate.backend.utility : tokenize;
237 
238         return tokenize(ctx, p);
239     }
240 
241     Token[] getFilteredTokens(Path p) {
242         import std.array : array;
243         import std.algorithm : filter;
244         import clang.c.Index : CXTokenKind;
245         import dextool.plugin.mutate.backend.utility : tokenize;
246 
247         // Filter a stream of tokens for those that should affect the checksum.
248         return tokenize(ctx, p).filter!(a => a.kind != CXTokenKind.comment).array;
249     }
250 }
251 
252 enum FileStatus {
253     noChange,
254     notInDatabase,
255     changed
256 }
257 
258 /// Print the files that has been removed from the database since last analysis.
259 void printPrunedFiles(ref Set!Path before_files,
260         ref Set!AbsolutePath analyzed_files, const AbsolutePath root_dir) @safe {
261     import dextool.type : FileName;
262 
263     foreach (const f; setToRange!Path(before_files)) {
264         auto abs_f = AbsolutePath(FileName(f), DirName(cast(string) root_dir));
265         logger.infof(!analyzed_files.contains(abs_f), "Removed from files to mutate: '%s'", abs_f);
266     }
267 }
268 
269 FileStatus isFileChanged(ref Database db, Path relp, Checksum f_checksum) @safe {
270     if (!db.isAnalyzed(relp))
271         return FileStatus.notInDatabase;
272 
273     auto db_checksum = db.getFileChecksum(relp);
274 
275     auto rval = (!db_checksum.isNull && db_checksum != f_checksum) ? FileStatus.changed
276         : FileStatus.noChange;
277     debug logger.trace(rval == FileStatus.changed, "db: ", db_checksum, " file: ", f_checksum);
278 
279     return rval;
280 }