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 }