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, FileName, 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.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 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 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 struct McCabe { 109 import std.typecons : NullableRef; 110 111 private McCabeResult result; 112 113 this(McCabeResult result) { 114 this.result = result; 115 } 116 117 void analyze(T)(const(T) v) @safe { 118 auto c = v.cursor; 119 120 if (!c.isDefinition) { 121 return; 122 } 123 124 auto mccabe = () @trusted{ 125 import dextool.clang_extensions; 126 127 return calculate(c.cx); 128 }(); 129 130 if (!mccabe.hasValue || mccabe.value == 0) 131 return; 132 133 auto loc = c.location; 134 auto file_under_analyze = AbsolutePath(FileName(loc.file.toString)); 135 136 import cpptooling.data.type : CFunctionName; 137 138 result.put(Function(CFunctionName(c.spelling), file_under_analyze, 139 loc.line, loc.column, mccabe.value)); 140 } 141 } 142 143 /** 144 * Trusted: only the assocative array is problematic. 145 */ 146 void resultToStdout(McCabeResult analyze, int threshold) @trusted { 147 import std.algorithm : map, filter; 148 import std.array : byPair; 149 import std.range : tee; 150 import std.stdio : writeln, writefln; 151 152 // the |==... is used to make it easy to filter with unix tools. It makes 153 // it so that sort -h will not mix it with the numbers. 154 155 long total; 156 157 writeln("McCabe Cyclomatic Complexity"); 158 writeln("|======File"); 159 foreach (f; analyze.files.byPair.map!(a => a[1]) 160 .tee!(a => total += a.complexity).filter!(a => a.complexity >= threshold)) { 161 writefln("%-6s %s", f.complexity, f.file); 162 } 163 writeln("|======Total McCabe ", total); 164 writeln("|======Function"); 165 foreach (f; analyze.functions[].filter!(a => a.complexity >= threshold)) 166 writefln("%-6s %s [%s line=%s column=%s]", f.complexity, 167 cast(string) f.name, cast(string) f.file, f.line, f.column); 168 169 if (analyze.files.length == 0 && analyze.functions.length == 0) { 170 writeln("No result. Did you forget --restrict?"); 171 } 172 } 173 174 /** 175 * Trusted: only the assocative array is problematic. 176 */ 177 void resultToJson(AbsolutePath fname, McCabeResult analyze, int threshold) @trusted { 178 import std.ascii : newline; 179 import std.algorithm : map, filter; 180 import std.array : byPair; 181 182 static import std.stdio; 183 184 auto fout = std.stdio.File(cast(string) fname, "w"); 185 186 fout.writeln(`{"kind": "files",`); 187 fout.write(` "values":[`); 188 long total; 189 bool add_comma; 190 foreach (f; analyze.files.byPair.map!(a => a[1])) { 191 if (add_comma) { 192 fout.writeln(","); 193 } else { 194 add_comma = true; 195 fout.writeln; 196 } 197 fout.writefln(` {"file":"%s",`, cast(string) f.file); 198 fout.writefln(` "mccabe":%s`, f.complexity); 199 fout.write(" }"); 200 total += f.complexity; 201 } 202 fout.writeln(newline, " ],"); 203 fout.writefln(` "total_mccabe":%s`, total); 204 fout.writeln("},"); 205 206 fout.writeln(`{"kind": "functions",`); 207 fout.write(` "values":[`); 208 add_comma = false; 209 foreach (f; analyze.functions[].filter!(a => a.complexity >= threshold)) { 210 if (add_comma) { 211 fout.writeln(","); 212 } else { 213 add_comma = true; 214 fout.writeln; 215 } 216 fout.writefln(` {"function":"%s",`, cast(string) f.name); 217 fout.writefln(` "location": { "file":"%s", "line":%s, "column":%s },`, 218 cast(string) f.file, f.line, f.column); 219 fout.writefln(` "mccabe":%s`, f.complexity); 220 fout.write(" }"); 221 } 222 223 fout.writeln(newline, " ]"); 224 fout.writeln("}"); 225 }