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);