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 }