1 module cogito.meter;
2 
3 import core.stdc.stdarg;
4 import core.stdc.stdio : fputc, fputs, fprintf, stderr;
5 import dmd.frontend;
6 import dmd.identifier;
7 import dmd.globals;
8 import dmd.console;
9 import dmd.root.outbuffer;
10 
11 import cogito.arguments;
12 import cogito.list;
13 import std.algorithm;
14 import std.conv;
15 import std.range;
16 import std.stdio : write, writefln;
17 import std.typecons;
18 import std.sumtype;
19 import std.traits;
20 
21 private mixin template Ruler()
22 {
23     uint ownScore = 0;
24     List!Meter inner;
25 
26     @disable this();
27 
28     public uint score()
29     {
30         return this.ownScore
31             + reduce!((accum, x) => accum + x.score)(0, this.inner[]);
32     }
33 }
34 
35 /**
36  * Identifier and its location in the source file.
37  */
38 struct ScoreScope
39 {
40     /**
41      * Declaration identifier (e.g. function or struct name, may be empty if
42      * this is a lambda).
43      */
44     Identifier identifier;
45 
46     /// Source position.
47     Loc location;
48 }
49 
50 /**
51  * Collects the score from a single declaration, like a function. Can contain
52  * nested $(D_SYMBOL Meter) structures with nested declarations.
53  */
54 struct Meter
55 {
56     private ScoreScope scoreScope;
57 
58     /// Symbol type.
59     enum Type
60     {
61         aggregate, /// Aggregate.
62         callable /// Function.
63     }
64     private Type type;
65 
66     /// Gets the evaluated identifier.
67     @property ref Identifier identifier() return
68     {
69         return this.scoreScope.identifier;
70     }
71 
72     /// Sets the evaluated identifier.
73     @property void identifier(ref Identifier identifier)
74     {
75         this.scoreScope.identifier = identifier;
76     }
77 
78     @property const(char)[] name()
79     {
80         auto stringName = this.scoreScope.identifier.toString();
81 
82         if (stringName.empty)
83         {
84             return "(λ)";
85         }
86         else if (stringName == "__ctor")
87         {
88             return "this";
89         }
90         else if (stringName == "__dtor")
91         {
92             return "~this";
93         }
94         else if (stringName == "__postblit")
95         {
96             return "this(this)";
97         }
98         else if (stringName.startsWith("_sharedStaticCtor_"))
99         {
100             return "shared static this";
101         }
102         else if (stringName.startsWith("_sharedStaticDtor_"))
103         {
104             return "shared static ~this";
105         }
106         else
107         {
108             return stringName;
109         }
110     }
111 
112     /// Gets identifier location.
113     @property ref Loc location() return
114     {
115         return this.scoreScope.location;
116     }
117 
118     /// Sets identifier location.
119     @property void location(ref Loc location)
120     {
121         this.scoreScope.location = location;
122     }
123 
124     /**
125      * Params:
126      *     identifier = Identifier.
127      *     location = Identifier location.
128      *     type = Symbol type.
129      */
130     public this(Identifier identifier, Loc location, Type type)
131     {
132         this.identifier = identifier;
133         this.location = location;
134         this.type = type;
135     }
136 
137     /**
138      * Returns: $(D_KEYWORD true) if any function inside the current node
139      *          excceds the threshold, otherwise $(D_KEYWORD false).
140      */
141     bool isAbove(uint threshold)
142     {
143         if (threshold == 0)
144         {
145             return false;
146         }
147         if (this.type == Type.callable)
148         {
149             return this.score > threshold;
150         }
151         else
152         {
153             return reduce!((accum, x) => accum || x.isAbove(threshold))(false, this.inner[]);
154         }
155     }
156 
157     mixin Ruler!();
158 }
159 
160 /**
161  * Prints the information about the given identifier.
162  *
163  * Params:
164  *     sink = Function used to print the information.
165  */
166 struct VerboseReporter(alias sink)
167 if (isCallable!sink)
168 {
169     private Source source;
170 
171     @disable this();
172 
173     this(Source source)
174     {
175         this.source = source;
176     }
177 
178     /**
179      * Params:
180      *     meter = The score statistics to print.
181      */
182     void report()
183     {
184         sink(this.source.moduleName);
185         sink(": ");
186         sink(this.source.score.to!string);
187         sink("\n");
188 
189         foreach (ref meter; this.source.inner[])
190         {
191             traverse(meter, 1);
192         }
193     }
194 
195     private void traverse(ref Meter meter, const uint indentation)
196     {
197         const indentBytes = ' '.repeat(indentation * 2).array;
198         const nextIndentation = indentation + 1;
199         const nextIndentBytes = ' '.repeat(nextIndentation * 2).array;
200 
201         sink(indentBytes);
202         sink(meter.name);
203 
204         sink(":\n");
205         sink(nextIndentBytes);
206         sink("Location: ");
207         sink(to!string(meter.location.linnum));
208         sink(":");
209         sink(to!string(meter.location.charnum));
210         sink("\n");
211         sink(nextIndentBytes);
212         sink("Score: ");
213 
214         sink(meter.score.to!string);
215         sink("\n");
216 
217         meter.inner[].each!(meter => this.traverse(meter, nextIndentation));
218     }
219 }
220 
221 /**
222  * Prints the information about the given identifier.
223  *
224  * Params:
225  *     sink = Function used to print the information.
226  */
227 struct FlatReporter(alias sink)
228 if (isCallable!sink)
229 {
230     private Source source;
231 
232     @disable this();
233 
234     /**
235      * Params:
236      *     source = Scores collected from a source file.
237      */
238     this(Source source)
239     {
240         this.source = source;
241     }
242 
243     /**
244      * Params:
245      *     threshold = Function score limit.
246      *     moduleThreshold = Module score limit.
247      */
248     void report(uint threshold, uint moduleThreshold)
249     {
250         const sourceScore = this.source.score;
251 
252         if ((threshold == 0 && moduleThreshold == 0)
253                 || (moduleThreshold != 0 && sourceScore > moduleThreshold))
254         {
255             sink("module ");
256             sink(this.source.moduleName);
257             sink(": ");
258             sink(sourceScore.to!string);
259             sink(" (");
260             sink(this.source.filename);
261             sink(")");
262             sink("\n");
263         }
264 
265         foreach (ref meter; this.source.inner[])
266         {
267             traverse(meter, threshold, []);
268         }
269     }
270 
271     private void traverse(ref Meter meter,
272             uint threshold, const string[] path)
273     {
274         if (threshold != 0 && !meter.isAbove(threshold))
275         {
276             return;
277         }
278         const nameParts = path ~ [meter.name.idup];
279 
280         if (meter.type == Meter.Type.callable)
281         {
282             sink(nameParts.join("."));
283             sink(": ");
284             sink(meter.score.to!string);
285             sink(" (");
286             sink(this.source.filename);
287             sink(":");
288             sink(to!string(meter.location.linnum));
289             sink(")");
290             sink("\n");
291         }
292 
293         meter.inner[].each!(meter => this.traverse(meter,
294                 threshold, nameParts));
295     }
296 }
297 
298 /**
299  * Collects the score from a single D module.
300  */
301 struct Source
302 {
303     /// Module name.
304     string moduleName = "main";
305 
306     /// Module file name.
307     private string filename_;
308 
309     /**
310      * Params:
311      *     inner = List with module metrics.
312      *     filename = Module file name.
313      */
314     this(List!Meter inner, string filename = "-")
315     @nogc nothrow pure @safe
316     {
317         this.inner = inner;
318         this.filename_ = filename;
319     }
320 
321     @property string filename() @nogc nothrow pure @safe
322     {
323         return this.filename_;
324     }
325 
326     /**
327      * Returns: $(D_KEYWORD true) if any function inside the current node
328      *          excceds the threshold, otherwise $(D_KEYWORD false).
329      */
330     bool isAbove(uint threshold)
331     {
332         if (threshold == 0)
333         {
334             return false;
335         }
336         return reduce!((accum, x) => accum || x.isAbove(threshold))(false, this.inner[]);
337     }
338 
339     mixin Ruler!();
340 }
341 
342 /**
343  * Prints source file metrics to the standard output.
344  *
345  * Params:
346  *     source = Collected metrics and scores.
347  *     threshold = Maximum acceptable function score.
348  *     moduleThreshold = Maximum acceptable module score.
349  *     format = Output format.
350  *
351  * Returns: $(D_KEYWORD true) if the score exceeds the threshold, otherwise
352  *          returns $(D_KEYWORD false).
353  */
354 bool report(Source source, uint threshold,
355         uint moduleThreshold, OutputFormat format)
356 {
357     const sourceScore = source.score;
358     const bool aboveModuleThreshold = moduleThreshold != 0
359         && sourceScore > moduleThreshold;
360     const bool aboveAnyThreshold = aboveModuleThreshold || source.isAbove(threshold);
361     if (format == nullable(OutputFormat.silent))
362     {
363         return aboveAnyThreshold;
364     }
365 
366     if (format == nullable(OutputFormat.verbose))
367     {
368         VerboseReporter!write(source).report();
369     }
370     else if (aboveAnyThreshold || (moduleThreshold == 0 && threshold == 0))
371     {
372         FlatReporter!write(source).report(threshold, moduleThreshold);
373     }
374 
375     return aboveAnyThreshold;
376 }
377 
378 /**
379  * Prints an error list to the standard output.
380  *
381  * Params:
382  *     errors = The errors to print.
383  */
384 void printErrors(List!CognitiveError errors)
385 {
386     foreach (error; errors[])
387     {
388         auto location = error.location.toChars();
389 
390         if (*location)
391         {
392             fprintf(stderr, "%s: ", location);
393         }
394         fputs(error.header, stderr);
395 
396         fputs(error.message.peekChars(), stderr);
397         fputc('\n', stderr);
398     }
399 }
400 
401 struct CognitiveError
402 {
403     Loc location;
404     Color headerColor;
405     const(char)* header;
406     RefCounted!OutBuffer message;
407 }
408 
409 struct LocalHandler
410 {
411     List!CognitiveError errors;
412 
413     bool handler(const ref Loc location,
414         Color headerColor,
415         const(char)* header,
416         const(char)* messageFormat,
417         va_list args,
418         const(char)* prefix1,
419         const(char)* prefix2) nothrow
420     {
421         CognitiveError error;
422 
423         error.location = location;
424         error.headerColor = headerColor;
425         error.header = header;
426 
427         if (prefix1)
428         {
429             error.message.writestring(prefix1);
430             error.message.writestring(" ");
431         }
432         if (prefix2)
433         {
434             error.message.writestring(prefix2);
435             error.message.writestring(" ");
436         }
437         error.message.vprintf(messageFormat, args);
438 
439         this.errors.insert(error);
440 
441         return true;
442     }
443 }
444 
445 /**
446  * Initialize global variables.
447  */
448 void initialize()
449 {
450     initDMD(null, [],
451         ContractChecks(
452             ContractChecking.default_,
453             ContractChecking.default_,
454             ContractChecking.default_,
455             ContractChecking.default_,
456             ContractChecking.default_,
457             ContractChecking.default_
458         )
459     );
460 }
461 
462 /**
463  * Clean up global variables.
464  */
465 void deinitialize()
466 {
467     deinitializeDMD();
468 }
469 
470 /// Result of analysing a source file.
471 alias Result = SumType!(List!CognitiveError, Source);