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         class_, /// Class.
64         interface_, /// Interface.
65         struct_, /// Struct.
66         template_, /// Template.
67         union_, /// Union.
68     }
69     private Type type;
70 
71     /// Gets the evaluated identifier.
72     @property ref Identifier identifier() return
73     {
74         return this.scoreScope.identifier;
75     }
76 
77     /// Sets the evaluated identifier.
78     @property void identifier(ref Identifier identifier)
79     {
80         this.scoreScope.identifier = identifier;
81     }
82 
83     @property const(char)[] name()
84     {
85         auto stringName = this.scoreScope.identifier.toString();
86 
87         if (stringName.empty)
88         {
89             return "(λ)";
90         }
91         else if (stringName == "__ctor")
92         {
93             return "this";
94         }
95         else if (stringName == "__dtor")
96         {
97             return "~this";
98         }
99         else if (stringName == "__postblit")
100         {
101             return "this(this)";
102         }
103         else if (stringName.startsWith("_sharedStaticCtor_"))
104         {
105             return "shared static this";
106         }
107         else if (stringName.startsWith("_sharedStaticDtor_"))
108         {
109             return "shared static ~this";
110         }
111         else
112         {
113             return stringName;
114         }
115     }
116 
117     /// Gets identifier location.
118     @property ref Loc location() return
119     {
120         return this.scoreScope.location;
121     }
122 
123     /// Sets identifier location.
124     @property void location(ref Loc location)
125     {
126         this.scoreScope.location = location;
127     }
128 
129     /**
130      * Params:
131      *     identifier = Identifier.
132      *     location = Identifier location.
133      *     type = Symbol type.
134      */
135     public this(Identifier identifier, Loc location, Type type)
136     {
137         this.identifier = identifier;
138         this.location = location;
139         this.type = type;
140     }
141 
142     /**
143      * Returns: Type of the exceeded threshold or null.
144      */
145     Nullable!(Threshold.Type) isAbove(Threshold threshold)
146     {
147         if (threshold.noneSet)
148         {
149             return typeof(return)();
150         }
151         if (threshold.function_ != 0
152                 && this.type == Type.callable
153                 && this.score > threshold.function_)
154         {
155             return nullable(Threshold.Type.function_);
156         }
157         else if (threshold.aggregate != 0
158                 && this.type != Type.callable // Aggregate.
159                 && this.score > threshold.aggregate)
160         {
161             return nullable(Threshold.Type.aggregate);
162         }
163         else
164         {
165             return reduce!((accum, x) => accum.isNull ? x.isAbove(threshold) : accum)(typeof(return)(), this.inner[]);
166         }
167     }
168 
169     mixin Ruler!();
170 }
171 
172 private string typeToString(Meter.Type meterType)
173 {
174     final switch(meterType) with (Meter.Type)
175     {
176         case aggregate:
177              return "aggregate";
178         case callable:
179              return "function";
180         case class_:
181              return "class";
182         case interface_:
183              return "interface";
184         case struct_:
185              return "struct";
186         case template_:
187              return "template";
188         case union_:
189              return "union";
190     }
191 }
192 
193 /**
194  * Prints the information about the given identifier.
195  *
196  * Params:
197  *     sink = Function used to print the information.
198  */
199 struct DebugReporter(alias sink)
200 if (isCallable!sink)
201 {
202     private Source source;
203 
204     @disable this();
205 
206     this(Source source)
207     {
208         this.source = source;
209     }
210 
211     /**
212      * Params:
213      *     meter = The score statistics to print.
214      */
215     void report()
216     {
217         sink(this.source.moduleName);
218         sink(": ");
219         sink(this.source.score.to!string);
220         sink("\n");
221 
222         foreach (ref meter; this.source.inner[])
223         {
224             traverse(meter, 1);
225         }
226     }
227 
228     private void traverse(ref Meter meter, const uint indentation)
229     {
230         const indentBytes = ' '.repeat(indentation * 2).array;
231         const nextIndentation = indentation + 1;
232         const nextIndentBytes = ' '.repeat(nextIndentation * 2).array;
233 
234         sink(indentBytes);
235         sink(meter.name);
236 
237         sink(":\n");
238         sink(nextIndentBytes);
239         sink("Location: ");
240         sink(to!string(meter.location.linnum));
241         sink(":");
242         sink(to!string(meter.location.charnum));
243         sink("\n");
244         sink(nextIndentBytes);
245         sink("Score: ");
246 
247         sink(meter.score.to!string);
248         sink("\n");
249 
250         meter.inner[].each!(meter => this.traverse(meter, nextIndentation));
251     }
252 }
253 
254 /**
255  * Prints the information about the given identifier.
256  *
257  * Params:
258  *     sink = Function used to print the information.
259  */
260 struct FlatReporter(alias sink)
261 if (isCallable!sink)
262 {
263     private Source source;
264 
265     @disable this();
266 
267     /**
268      * Params:
269      *     source = Scores collected from a source file.
270      */
271     this(Source source)
272     {
273         this.source = source;
274     }
275 
276     /**
277      * Params:
278      *     threshold = Score limits.
279      */
280     void report(Threshold threshold)
281     {
282         const sourceScore = this.source.score;
283 
284         if (threshold.noneSet
285                 || (threshold.module_ != 0 && sourceScore > threshold.module_))
286         {
287             sink("module ");
288             sink(this.source.moduleName);
289             sink(": ");
290             sink(sourceScore.to!string);
291             sink(" (");
292             sink(this.source.filename);
293             sink(")");
294             sink("\n");
295         }
296 
297         foreach (ref meter; this.source.inner[])
298         {
299             traverse(meter, threshold, []);
300         }
301     }
302 
303     private void traverse(ref Meter meter,
304             Threshold threshold, const string[] path)
305     {
306         const noneSet = threshold.noneSet;
307         const exceededThreshold = meter.isAbove(threshold);
308 
309         if (exceededThreshold.isNull && !noneSet)
310         {
311             return;
312         }
313         const nameParts = path ~ [meter.name.idup];
314 
315         if (noneSet
316                 || (meter.type == Meter.Type.callable && exceededThreshold == nullable(Threshold.Type.function_))
317                 || (meter.type != Meter.Type.callable && exceededThreshold == nullable(Threshold.Type.aggregate)))
318         {
319             sink(this.source.filename);
320             sink(":");
321             sink(to!string(meter.location.linnum));
322             sink(": ");
323             sink(typeToString(meter.type));
324             sink(" ");
325             sink(nameParts.join("."));
326             sink(": ");
327             sink(meter.score.to!string);
328             sink("\n");
329         }
330         meter.inner[].each!(meter => this.traverse(meter, threshold, nameParts));
331     }
332 }
333 
334 /**
335  * Collects the score from a single D module.
336  */
337 struct Source
338 {
339     /// Module name.
340     string moduleName = "main";
341 
342     /// Module file name.
343     private string filename_;
344 
345     /**
346      * Params:
347      *     inner = List with module metrics.
348      *     filename = Module file name.
349      */
350     this(List!Meter inner, string filename = "-")
351     @nogc nothrow pure @safe
352     {
353         this.inner = inner;
354         this.filename_ = filename;
355     }
356 
357     @property string filename() @nogc nothrow pure @safe
358     {
359         return this.filename_;
360     }
361 
362     /**
363      * Returns: Type of the exceeded threshold or null.
364      */
365     Nullable!(Threshold.Type) isAbove(Threshold threshold)
366     {
367         if (threshold.noneSet)
368         {
369             return typeof(return)();
370         }
371         else if (threshold.module_ != 0 && this.score > threshold.module_)
372         {
373             return nullable(Threshold.Type.module_);
374         }
375         else
376         {
377             return reduce!((accum, x) => accum.isNull ? x.isAbove(threshold) : accum)(typeof(return)(), this.inner[]);
378         }
379     }
380 
381     mixin Ruler!();
382 }
383 
384 /**
385  * Supported threshold values.
386  */
387 struct Threshold
388 {
389     /**
390      * Available thresholds.
391      */
392     enum Type
393     {
394         function_, /// Function.
395         aggregate, /// Aggregate.
396         module_, /// Module.
397     }
398 
399     /// Function threshold.
400     uint function_;
401 
402     /// Aggregate threshold.
403     uint aggregate;
404 
405     /// Module threshold.
406     uint module_;
407 
408     /**
409      * Returns: Whether none threshold is set.
410      */
411     bool noneSet()
412     {
413         return this.function_ == 0 && this.aggregate == 0 && this.module_ == 0;
414     }
415 }
416 
417 /**
418  * Prints source file metrics to the standard output.
419  *
420  * Params:
421  *     source = Collected metrics and scores.
422  *     threshold = Maximum acceptable scores.
423  *     format = Output format.
424  *
425  * Returns: $(D_KEYWORD true) if the score exceeds the threshold, otherwise
426  *          returns $(D_KEYWORD false).
427  */
428 Nullable!(Threshold.Type) report(Source source, Threshold threshold, OutputFormat format)
429 {
430     const aboveAnyThreshold = source.isAbove(threshold);
431 
432     if (format == OutputFormat.silent)
433     {
434         return aboveAnyThreshold;
435     }
436     else if (format == OutputFormat.debug_)
437     {
438         DebugReporter!write(source).report();
439     }
440     else if (format == OutputFormat.verbose)
441     {
442         FlatReporter!write(source).report(Threshold());
443     }
444     else if (!aboveAnyThreshold.isNull || threshold.noneSet)
445     {
446         FlatReporter!write(source).report(threshold);
447     }
448 
449     return aboveAnyThreshold;
450 }
451 
452 /**
453  * Prints an error list to the standard output.
454  *
455  * Params:
456  *     errors = The errors to print.
457  */
458 void printErrors(List!CognitiveError errors)
459 {
460     foreach (error; errors[])
461     {
462         auto location = error.location.toChars();
463 
464         if (*location)
465         {
466             fprintf(stderr, "%s: ", location);
467         }
468         fputs(error.header, stderr);
469 
470         fputs(error.message.peekChars(), stderr);
471         fputc('\n', stderr);
472     }
473 }
474 
475 struct CognitiveError
476 {
477     Loc location;
478     Color headerColor;
479     const(char)* header;
480     RefCounted!OutBuffer message;
481 }
482 
483 struct LocalHandler
484 {
485     List!CognitiveError errors;
486 
487     bool handler(const ref Loc location,
488         Color headerColor,
489         const(char)* header,
490         const(char)* messageFormat,
491         va_list args,
492         const(char)* prefix1,
493         const(char)* prefix2) nothrow
494     {
495         CognitiveError error;
496 
497         error.location = location;
498         error.headerColor = headerColor;
499         error.header = header;
500 
501         if (prefix1)
502         {
503             error.message.writestring(prefix1);
504             error.message.writestring(" ");
505         }
506         if (prefix2)
507         {
508             error.message.writestring(prefix2);
509             error.message.writestring(" ");
510         }
511         error.message.vprintf(messageFormat, args);
512 
513         this.errors.insert(error);
514 
515         return true;
516     }
517 }
518 
519 /**
520  * Initialize global variables.
521  */
522 void initialize()
523 {
524     initDMD(null, [],
525         ContractChecks(
526             ContractChecking.default_,
527             ContractChecking.default_,
528             ContractChecking.default_,
529             ContractChecking.default_,
530             ContractChecking.default_,
531             ContractChecking.default_
532         )
533     );
534 }
535 
536 /**
537  * Clean up global variables.
538  */
539 void deinitialize()
540 {
541     deinitializeDMD();
542 }
543 
544 /// Result of analysing a source file.
545 alias Result = SumType!(List!CognitiveError, Source);