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 }