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 module dextool.plugin.analyze.mccabe; 11 12 import logger = std.experimental.logger; 13 14 import dextool.type : ExitStatusType, Path, AbsolutePath; 15 16 @safe: 17 18 struct Function { 19 import cpptooling.data.type : CFunctionName; 20 21 CFunctionName name; 22 23 AbsolutePath file; 24 uint line; 25 uint column; 26 27 int complexity; 28 29 nothrow @safe size_t toHash() { 30 import std.digest; 31 import std.digest.crc; 32 33 auto hash = makeDigest!CRC32(); 34 () @trusted { 35 hash.put(cast(const(ubyte)[]) cast(string) file); 36 hash.put(cast(const(ubyte)[]) cast(string) name); 37 }(); 38 hash.finish; 39 40 auto h = hash.peek(); 41 return line ^ column ^ (h[0] << 24 | h[1] << 16 | h[2] << 8 | h[3]); 42 } 43 44 bool opEquals(const typeof(this) o) @safe pure nothrow const @nogc { 45 return name == o.name && file == o.file && line == o.line && column == o.column; 46 } 47 48 int opCmp(const typeof(this) rhs) @safe pure nothrow const { 49 // dfmt off 50 if (name < rhs.name) 51 return -1; 52 else if (name > rhs.name) 53 return 1; 54 if (file < rhs.file) 55 return -1; 56 else if (file > rhs.file) 57 return 1; 58 if (line < rhs.line) 59 return -1; 60 else if (line > rhs.line) 61 return 1; 62 if (column < rhs.column) 63 return -1; 64 else if (column > rhs.column) 65 return 1; 66 return this == rhs ? 0 : 1; 67 // dfmt on 68 } 69 } 70 71 struct File { 72 AbsolutePath file; 73 int complexity; 74 } 75 76 class McCabeResult { 77 import std.container : RedBlackTree; 78 79 RedBlackTree!Function functions; 80 File[AbsolutePath] files; 81 82 this() { 83 import std.container : make; 84 85 this.functions = make!(typeof(functions)); 86 } 87 88 /** 89 * Returns: if the function where added. 90 */ 91 void put(Function func) @safe { 92 // unsafe in dmd-2.071.1 but safe in 2.075.0 93 auto insert_nr = () @trusted { return functions.insert(func); }(); 94 95 if (insert_nr == 1) { 96 // files that are inserted are thus unique in the analyze. 97 // it is thus OK to add the mccabe to the file count. 98 99 if (auto f = func.file in files) { 100 f.complexity += func.complexity; 101 } else { 102 files[func.file] = File(func.file, func.complexity); 103 } 104 } 105 } 106 } 107 108 class McCabe { 109 private McCabeResult result; 110 111 this(McCabeResult result) { 112 this.result = result; 113 } 114 115 void analyze(T)(const(T) v) @safe { 116 auto c = v.cursor; 117 118 if (!c.isDefinition) { 119 return; 120 } 121 122 auto mccabe = () @trusted { 123 import dextool.clang_extensions; 124 125 return calculate(c.cx); 126 }(); 127 128 if (!mccabe.hasValue || mccabe.value == 0) 129 return; 130 131 auto loc = c.location; 132 auto file_under_analyze = AbsolutePath(Path(loc.file.toString)); 133 134 import cpptooling.data.type : CFunctionName; 135 136 result.put(Function(CFunctionName(c.spelling), file_under_analyze, 137 loc.line, loc.column, mccabe.value)); 138 } 139 } 140 141 /** 142 * Trusted: only the assocative array is problematic. 143 */ 144 void resultToStdout(McCabeResult analyze, int threshold) @trusted { 145 import std.algorithm : map, filter; 146 import std.array : byPair; 147 import std.range : tee; 148 import std.stdio : writeln, writefln; 149 150 // the |==... is used to make it easy to filter with unix tools. It makes 151 // it so that sort -h will not mix it with the numbers. 152 153 long total; 154 155 writeln("McCabe Cyclomatic Complexity"); 156 writeln("|======File"); 157 foreach (f; analyze.files 158 .byPair 159 .map!(a => a[1]) 160 .tee!(a => total += a.complexity) 161 .filter!(a => a.complexity >= threshold)) { 162 writefln("%-6s %s", f.complexity, f.file); 163 } 164 writeln("|======Total McCabe ", total); 165 writeln("|======Function"); 166 foreach (f; analyze.functions[].filter!(a => a.complexity >= threshold)) 167 writefln("%-6s %s [%s line=%s column=%s]", f.complexity, 168 cast(string) f.name, cast(string) f.file, f.line, f.column); 169 170 if (analyze.files.length == 0 && analyze.functions.length == 0) { 171 writeln("No result. Did you forget --restrict?"); 172 } 173 } 174 175 /** 176 * Trusted: only the assocative array is problematic. 177 */ 178 void resultToJson(AbsolutePath fname, McCabeResult analyze, int threshold) @trusted { 179 import std.ascii : newline; 180 import std.algorithm : map, filter; 181 import std.array : byPair; 182 183 static import std.stdio; 184 185 auto fout = std.stdio.File(cast(string) fname, "w"); 186 187 fout.writeln("["); 188 fout.writeln(`{"kind": "files",`); 189 fout.write(` "values":[`); 190 long total; 191 bool add_comma; 192 foreach (f; analyze.files.byPair.map!(a => a[1])) { 193 if (add_comma) { 194 fout.writeln(","); 195 } else { 196 add_comma = true; 197 fout.writeln; 198 } 199 fout.writefln(` {"file":"%s",`, cast(string) f.file); 200 fout.writefln(` "mccabe":%s`, f.complexity); 201 fout.write(" }"); 202 total += f.complexity; 203 } 204 fout.writeln(newline, " ],"); 205 fout.writefln(` "total_mccabe":%s`, total); 206 fout.writeln("},"); 207 208 fout.writeln(`{"kind": "functions",`); 209 fout.write(` "values":[`); 210 add_comma = false; 211 foreach (f; analyze.functions[].filter!(a => a.complexity >= threshold)) { 212 if (add_comma) { 213 fout.writeln(","); 214 } else { 215 add_comma = true; 216 fout.writeln; 217 } 218 fout.writefln(` {"function":"%s",`, cast(string) f.name); 219 fout.writefln(` "location": { "file":"%s", "line":%s, "column":%s },`, 220 cast(string) f.file, f.line, f.column); 221 fout.writefln(` "mccabe":%s`, f.complexity); 222 fout.write(" }"); 223 } 224 225 fout.writeln(newline, " ]"); 226 fout.writeln("}"); 227 fout.writeln("]"); 228 }