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