Appearance
| 1 | namespace Logging is | |
| 2 | use System.Exception | |
| 3 | ||
| 4 | use Collections.Map | |
| 5 | use Collections.MutableMap | |
| 6 | use Collections.List | |
| 7 | use Collections.MutableList | |
| 8 | use Collections.Iterable | |
| 9 | ||
| 10 | use Collections.MAP | |
| 11 | use Collections.LIST | |
| 12 | use Collections.STACK | |
| 13 | ||
| 14 | use IO.TextWriter | |
| 15 | ||
| 16 | use Ghul.Pipes | |
| 17 | ||
| 18 | use Source.LOCATION | |
| 19 | ||
| 20 | // A secondary location a diagnostic points at, alongside its own | |
| 21 | // primary location - a prior declaration, the base member being | |
| 22 | // overridden, the call that invalidated a narrowing. An editor | |
| 23 | // renders this as a clickable cross-reference (LSP | |
| 24 | // DiagnosticRelatedInformation); the text formatters append it to | |
| 25 | // the line they print. `message` here is terse by comparison, so the | |
| 26 | // diagnostic's own `text` still has to name the thing it points at | |
| 27 | // - the related location says where, not what. | |
| 28 | struct RELATED_LOCATION is | |
| 29 | location: LOCATION | |
| 30 | message: string | |
| 31 | ||
| 32 | init(location: LOCATION, message: string) is | |
| 33 | self.location = location | |
| 34 | self.message = message | |
| 35 | si | |
| 36 | si | |
| 37 | ||
| 38 | struct DIAGNOSTIC_MESSAGE is | |
| 39 | is_analysis: bool | |
| 40 | is_compile_expressions: bool | |
| 41 | ||
| 42 | severity: DiagnosticSeverity | |
| 43 | location: LOCATION | |
| 44 | code: string? | |
| 45 | text: string | |
| 46 | // Other locations this diagnostic references. Null when there are | |
| 47 | // none. See RELATED_LOCATION. | |
| 48 | related: Collections.LIST[RELATED_LOCATION]? | |
| 49 | is_hint: bool => (severity == DiagnosticSeverity.HINT) | |
| 50 | is_fatal: bool => (severity == DiagnosticSeverity.FATAL \/ severity == DiagnosticSeverity.EXCEPTION) | |
| 51 | ||
| 52 | init( | |
| 53 | is_analysis: bool, | |
| 54 | is_compile_expressions: bool, | |
| 55 | severity: DiagnosticSeverity, | |
| 56 | location: LOCATION, | |
| 57 | text: string | |
| 58 | ) is | |
| 59 | self.is_analysis = is_analysis | |
| 60 | self.is_compile_expressions = is_compile_expressions | |
| 61 | self.severity = severity | |
| 62 | self.location = location | |
| 63 | self.code = null | |
| 64 | self.text = text | |
| 65 | self.related = null | |
| 66 | si | |
| 67 | ||
| 68 | init( | |
| 69 | is_analysis: bool, | |
| 70 | is_compile_expressions: bool, | |
| 71 | severity: DiagnosticSeverity, | |
| 72 | location: LOCATION, | |
| 73 | code: string?, | |
| 74 | text: string | |
| 75 | ) is | |
| 76 | self.is_analysis = is_analysis | |
| 77 | self.is_compile_expressions = is_compile_expressions | |
| 78 | self.severity = severity | |
| 79 | self.location = location | |
| 80 | self.code = code | |
| 81 | self.text = text | |
| 82 | self.related = null | |
| 83 | si | |
| 84 | ||
| 85 | init( | |
| 86 | is_analysis: bool, | |
| 87 | is_compile_expressions: bool, | |
| 88 | severity: DiagnosticSeverity, | |
| 89 | location: LOCATION, | |
| 90 | code: string?, | |
| 91 | text: string, | |
| 92 | related: Collections.LIST[RELATED_LOCATION]? | |
| 93 | ) is | |
| 94 | self.is_analysis = is_analysis | |
| 95 | self.is_compile_expressions = is_compile_expressions | |
| 96 | self.severity = severity | |
| 97 | self.location = location | |
| 98 | self.code = code | |
| 99 | self.text = text | |
| 100 | self.related = related | |
| 101 | si | |
| 102 | ||
| 103 | to_string() -> string => "{severity} {location} {text}" | |
| 104 | si | |
| 105 | ||
| 106 | // An editor-only inlay hint: ghost text at a source position, never | |
| 107 | // published as a diagnostic and surfaced only by the INLAY_HINTS | |
| 108 | // analysis query. Recorded in a per-path list of its own rather than | |
| 109 | // among the diagnostics, so every diagnostics-side count, walk and | |
| 110 | // clear is free of inlays and a kind producing one hint per statement | |
| 111 | // cannot tax the diagnostics pipeline. | |
| 112 | // | |
| 113 | // `text` is the ghost-text label; `detail` the hover payload - for a | |
| 114 | // narrowing-introduction carrier it holds only the narrowed-to type, | |
| 115 | // with the hover sentence assembled at read-out, and for a kill | |
| 116 | // carrier the full hover text. | |
| 117 | struct INLAY is | |
| 118 | is_analysis: bool | |
| 119 | is_compile_expressions: bool | |
| 120 | ||
| 121 | location: LOCATION | |
| 122 | code: string? | |
| 123 | text: string | |
| 124 | detail: string? | |
| 125 | ||
| 126 | init( | |
| 127 | is_analysis: bool, | |
| 128 | is_compile_expressions: bool, | |
| 129 | location: LOCATION, | |
| 130 | code: string?, | |
| 131 | text: string, | |
| 132 | detail: string? | |
| 133 | ) is | |
| 134 | self.is_analysis = is_analysis | |
| 135 | self.is_compile_expressions = is_compile_expressions | |
| 136 | self.location = location | |
| 137 | self.code = code | |
| 138 | self.text = text | |
| 139 | self.detail = detail | |
| 140 | si | |
| 141 | si | |
| 142 | ||
| 143 | trait DiagnosticFormatter is | |
| 144 | need_clear_errors: bool => false | |
| 145 | format(diagnostic: DIAGNOSTIC_MESSAGE) -> string? | |
| 146 | si | |
| 147 | ||
| 148 | class TAB_DELIMITED_DIAGNOSTIC_FORMATTER: DiagnosticFormatter is | |
| 149 | need_clear_errors: bool => true | |
| 150 | ||
| 151 | init() is | |
| 152 | si | |
| 153 | ||
| 154 | format(diagnostic: DIAGNOSTIC_MESSAGE) -> string? is | |
| 155 | let location = diagnostic.location | |
| 156 | let severity = | |
| 157 | if diagnostic.is_fatal then | |
| 158 | cast int(DiagnosticSeverity.HINT) | |
| 159 | else | |
| 160 | cast int(diagnostic.severity) | |
| 161 | fi | |
| 162 | ||
| 163 | let message = diagnostic.text | |
| 164 | let code = if diagnostic.code? then diagnostic.code; else ""; fi | |
| 165 | ||
| 166 | let s = string.format( | |
| 167 | "{{0}}\t{{1}}\t{{2}}\t{{3}}\t{{4}}\t{{5}}\t{{6}}\t{{7}}", | |
| 168 | [ | |
| 169 | location.file_name, | |
| 170 | location.start_line, | |
| 171 | location.start_column, | |
| 172 | location.end_line, | |
| 173 | location.end_column+1, | |
| 174 | cast int(severity), | |
| 175 | message, | |
| 176 | code | |
| 177 | ] | |
| 178 | ).replace('\n', ' ') | |
| 179 | ||
| 180 | return s | |
| 181 | si | |
| 182 | si | |
| 183 | ||
| 184 | class HUMAN_READABLE_DIAGNOSTIC_FORMATTER: DiagnosticFormatter is | |
| 185 | init() is | |
| 186 | si | |
| 187 | ||
| 188 | format(diagnostic: DIAGNOSTIC_MESSAGE) -> string? is | |
| 189 | if diagnostic.is_hint then | |
| 190 | return null | |
| 191 | fi | |
| 192 | ||
| 193 | let location = diagnostic.location | |
| 194 | let severity = diagnostic.severity.to_string().to_lower() | |
| 195 | let message = | |
| 196 | if diagnostic.code? then | |
| 197 | "[{diagnostic.code}] {diagnostic.text}" | |
| 198 | else | |
| 199 | diagnostic.text | |
| 200 | fi | |
| 201 | ||
| 202 | let s mut = string.format( | |
| 203 | "{{0}}: {{1}},{{2}}..{{3}},{{4}}: {{5}}: {{6}}", | |
| 204 | [ | |
| 205 | location.file_name, | |
| 206 | location.start_line, | |
| 207 | location.start_column, | |
| 208 | location.end_line, | |
| 209 | location.end_column+1, | |
| 210 | severity, | |
| 211 | message | |
| 212 | ] | |
| 213 | ) | |
| 214 | ||
| 215 | // Related locations are rendered textually so the CLI keeps | |
| 216 | // the information a capable client shows as a jump-to link. | |
| 217 | // One sitting on the diagnostic's own position is not a jump | |
| 218 | // target - advice carried this way rather than in the message | |
| 219 | // prose - so only its message is rendered. | |
| 220 | if let related = diagnostic.related then | |
| 221 | for r in related do | |
| 222 | let rl = r.location | |
| 223 | ||
| 224 | if rl =~ location then | |
| 225 | s = "{s} [{r.message}]" | |
| 226 | else | |
| 227 | s = "{s} [{r.message}: {rl.file_name}: {rl.start_line},{rl.start_column}..{rl.end_line},{rl.end_column+1}]" | |
| 228 | fi | |
| 229 | od | |
| 230 | fi | |
| 231 | ||
| 232 | return s | |
| 233 | si | |
| 234 | si | |
| 235 | ||
| 236 | // MSBuild's canonical error format: | |
| 237 | // | |
| 238 | // file(line,col,endLine,endCol): category code: text | |
| 239 | // | |
| 240 | // MSBuild recognises that shape and no other - a line it cannot parse | |
| 241 | // is logged as untyped message text, so the build reports no error at | |
| 242 | // the offending source position and counts no warnings at all. The | |
| 243 | // category has to be exactly "error" or "warning"; the severity's own | |
| 244 | // name is not one of those. The code slot takes the suppression slug, | |
| 245 | // which lets a consuming project reach a single diagnostic through | |
| 246 | // NoWarn / WarningsAsErrors / MSBuildWarningsAsMessages. | |
| 247 | class MSBUILD_DIAGNOSTIC_FORMATTER: DiagnosticFormatter is | |
| 248 | init() is | |
| 249 | si | |
| 250 | ||
| 251 | // The categories MSBuild parses. INFO has no counterpart, so it is | |
| 252 | // written as plain text rather than inflating the warning count. | |
| 253 | _category(severity: DiagnosticSeverity) -> string? static => | |
| 254 | if | |
| 255 | severity == DiagnosticSeverity.ERROR \/ | |
| 256 | severity == DiagnosticSeverity.FATAL \/ | |
| 257 | severity == DiagnosticSeverity.EXCEPTION | |
| 258 | then | |
| 259 | "error" | |
| 260 | elif severity == DiagnosticSeverity.WARN then | |
| 261 | "warning" | |
| 262 | else | |
| 263 | null | |
| 264 | fi | |
| 265 | ||
| 266 | // The format is one diagnostic per line, so a text carrying line | |
| 267 | // breaks would put everything after the first outside the shape | |
| 268 | // MSBuild parses. An EXCEPTION diagnostic is exactly that: its | |
| 269 | // text ends in a rendered .NET exception, stack trace included. | |
| 270 | _one_line(s: string) -> string static => | |
| 271 | s.replace('\n', ' ').replace('\r', ' ') | |
| 272 | ||
| 273 | format(diagnostic: Logging.DIAGNOSTIC_MESSAGE) -> string? is | |
| 274 | // Editor-only carriers never reach a build log. | |
| 275 | if diagnostic.is_hint then | |
| 276 | return null | |
| 277 | fi | |
| 278 | ||
| 279 | let location = diagnostic.location | |
| 280 | let message = diagnostic.text | |
| 281 | let category = _category(diagnostic.severity) | |
| 282 | let code = if diagnostic.code? then " {diagnostic.code}"; else ""; fi | |
| 283 | ||
| 284 | // The uncategorised (info) path keeps the same head shape | |
| 285 | // minus the category, so the shared tail below - code and | |
| 286 | // related locations - applies to it too: `--warn-as-info` | |
| 287 | // next to this flag must not drop the slug or the advice. | |
| 288 | let head mut = | |
| 289 | if category? then | |
| 290 | "{location.file_name}({location.start_line},{location.start_column},{location.end_line},{location.end_column+1}): {category}{code}: " | |
| 291 | else | |
| 292 | "{location.file_name}({location.start_line},{location.start_column},{location.end_line},{location.end_column+1}){code}: " | |
| 293 | fi | |
| 294 | ||
| 295 | let s mut = "{head}{message}" | |
| 296 | ||
| 297 | // Related locations are rendered textually so the CLI keeps | |
| 298 | // the information a capable client shows as a jump-to link. | |
| 299 | if let related = diagnostic.related then | |
| 300 | for r in related do | |
| 301 | let rl = r.location | |
| 302 | ||
| 303 | if rl =~ location then | |
| 304 | s = "{s} [{r.message}]" | |
| 305 | else | |
| 306 | s = "{s} [{r.message}: {rl.file_name}({rl.start_line},{rl.start_column})]" | |
| 307 | fi | |
| 308 | od | |
| 309 | fi | |
| 310 | ||
| 311 | return _one_line(s) | |
| 312 | si | |
| 313 | si | |
| 314 | ||
| 315 | class DIAGNOSTICS_LIST is | |
| 316 | _path: string | |
| 317 | _dirty: bool | |
| 318 | _deduplicator: DIAGNOSTIC_DEDUPLICATOR? | |
| 319 | _diagnostics: LIST[DIAGNOSTIC_MESSAGE] | |
| 320 | _inlays: LIST[INLAY] | |
| 321 | ||
| 322 | // Positions of the statement boundaries the parser inferred | |
| 323 | // (a terminator left off at end of line), as LOCATION-packed | |
| 324 | // ints. Appended in source order during parse, so sorted by | |
| 325 | // construction; homogeneous and per-statement, which is why | |
| 326 | // they do not share the INLAY record the text-carrying kinds | |
| 327 | // use. Wiped by clear(false), which every re-parse runs before | |
| 328 | // regenerating them. | |
| 329 | _inferred_terminators: LIST[int] | |
| 330 | ||
| 331 | is_poisoned: bool | |
| 332 | has_consumed_any: bool | |
| 333 | has_consumed_error: bool | |
| 334 | ||
| 335 | any_errors: bool => _diagnostics |> any(d => d.severity == DiagnosticSeverity.ERROR) | |
| 336 | any_warnings: bool => _diagnostics |> any(d => d.severity == DiagnosticSeverity.WARN) | |
| 337 | ||
| 338 | count: int => _diagnostics.count | |
| 339 | error_count: int => _diagnostics |> filter(d => d.severity == DiagnosticSeverity.ERROR) |> count() | |
| 340 | analysis_count: int => _diagnostics |> filter(d => d.is_analysis) |> count() | |
| 341 | ||
| 342 | diagnostics: List[DIAGNOSTIC_MESSAGE] => _diagnostics | |
| 343 | inlays: List[INLAY] => _inlays | |
| 344 | inferred_terminators: List[int] => _inferred_terminators | |
| 345 | ||
| 346 | init(path: string, deduplicate: bool) is | |
| 347 | _path = path | |
| 348 | ||
| 349 | if deduplicate then | |
| 350 | _deduplicator = DIAGNOSTIC_DEDUPLICATOR() | |
| 351 | fi | |
| 352 | ||
| 353 | _diagnostics = LIST() | |
| 354 | _inlays = LIST() | |
| 355 | _inferred_terminators = LIST() | |
| 356 | si | |
| 357 | ||
| 358 | add(diagnostic_message: DIAGNOSTIC_MESSAGE) is | |
| 359 | if let deduplicator = _deduplicator /\ !deduplicator.is_first(diagnostic_message) then | |
| 360 | return | |
| 361 | fi | |
| 362 | ||
| 363 | _dirty = true | |
| 364 | ||
| 365 | if diagnostic_message.severity == DiagnosticSeverity.FATAL \/ diagnostic_message.severity == DiagnosticSeverity.EXCEPTION then | |
| 366 | is_poisoned = true | |
| 367 | fi | |
| 368 | ||
| 369 | _diagnostics.add(diagnostic_message) | |
| 370 | si | |
| 371 | ||
| 372 | add_inlay(inlay: INLAY) is | |
| 373 | _dirty = true | |
| 374 | ||
| 375 | _inlays.add(inlay) | |
| 376 | si | |
| 377 | ||
| 378 | add_inferred_terminators(positions: Iterable[int]) is | |
| 379 | _dirty = true | |
| 380 | ||
| 381 | for p in positions do | |
| 382 | _inferred_terminators.add(p) | |
| 383 | od | |
| 384 | si | |
| 385 | ||
| 386 | mark_poisoned() is | |
| 387 | is_poisoned = true | |
| 388 | si | |
| 389 | ||
| 390 | mark_consumed_any() is | |
| 391 | has_consumed_any = true | |
| 392 | si | |
| 393 | ||
| 394 | mark_consumed_error() is | |
| 395 | has_consumed_error = true | |
| 396 | si | |
| 397 | ||
| 398 | clear_consumed_any() is | |
| 399 | has_consumed_any = false | |
| 400 | si | |
| 401 | ||
| 402 | clear_consumed_error() is | |
| 403 | has_consumed_error = false | |
| 404 | si | |
| 405 | ||
| 406 | clear(analysis_only: bool) is | |
| 407 | if analysis_only == false then | |
| 408 | _dirty = true | |
| 409 | _diagnostics.clear() | |
| 410 | _inlays.clear() | |
| 411 | _inferred_terminators.clear() | |
| 412 | else | |
| 413 | let n = LIST[DIAGNOSTIC_MESSAGE](_diagnostics.count) | |
| 414 | ||
| 415 | for d in _diagnostics do | |
| 416 | if !d.is_analysis then | |
| 417 | n.add(d) | |
| 418 | fi | |
| 419 | od | |
| 420 | ||
| 421 | _diagnostics = n | |
| 422 | ||
| 423 | let ni = LIST[INLAY](_inlays.count) | |
| 424 | ||
| 425 | for i in _inlays do | |
| 426 | if !i.is_analysis then | |
| 427 | ni.add(i) | |
| 428 | fi | |
| 429 | od | |
| 430 | ||
| 431 | _inlays = ni | |
| 432 | fi | |
| 433 | ||
| 434 | _reseed_deduplicator() | |
| 435 | si | |
| 436 | ||
| 437 | // The record of what has been reported has to match what | |
| 438 | // survives a clear, or a diagnostic that is cleared and then | |
| 439 | // reported again - a warning whose suppression was removed - | |
| 440 | // is taken for a repeat and dropped. | |
| 441 | _reseed_deduplicator() is | |
| 442 | if !_deduplicator? then | |
| 443 | return | |
| 444 | fi | |
| 445 | ||
| 446 | let deduplicator = DIAGNOSTIC_DEDUPLICATOR() | |
| 447 | ||
| 448 | for d in _diagnostics do | |
| 449 | let _ = deduplicator.is_first(d) | |
| 450 | od | |
| 451 | ||
| 452 | _deduplicator = deduplicator | |
| 453 | si | |
| 454 | ||
| 455 | clear_global_declaration_diagnostics() is | |
| 456 | let n = LIST[DIAGNOSTIC_MESSAGE](_diagnostics.count) | |
| 457 | ||
| 458 | for d in _diagnostics do | |
| 459 | if !d.is_analysis \/ d.is_compile_expressions then | |
| 460 | n.add(d) | |
| 461 | fi | |
| 462 | od | |
| 463 | ||
| 464 | _diagnostics = n | |
| 465 | ||
| 466 | let ni = LIST[INLAY](_inlays.count) | |
| 467 | ||
| 468 | for i in _inlays do | |
| 469 | if !i.is_analysis \/ i.is_compile_expressions then | |
| 470 | ni.add(i) | |
| 471 | fi | |
| 472 | od | |
| 473 | ||
| 474 | _inlays = ni | |
| 475 | ||
| 476 | _reseed_deduplicator() | |
| 477 | si | |
| 478 | ||
| 479 | // The complement of clear_global_declaration_diagnostics: drop | |
| 480 | // only the expression-level diagnostics, keeping parse and | |
| 481 | // declaration-level ones. Used before a compile-expressions | |
| 482 | // re-walk of one file, which re-reports the expression | |
| 483 | // diagnostics but re-runs no earlier pass - the analysis-mode | |
| 484 | // on-demand recompile, and a batch build that recompiles a | |
| 485 | // file's expressions between solve rounds. | |
| 486 | // The same, for the expression-level output of one definition | |
| 487 | // whose body alone is being re-walked. | |
| 488 | clear_expression_diagnostics_within(span: LOCATION) is | |
| 489 | let n = LIST[DIAGNOSTIC_MESSAGE](_diagnostics.count) | |
| 490 | ||
| 491 | for d in _diagnostics do | |
| 492 | if !(d.is_compile_expressions /\ span.contains(d.location)) then | |
| 493 | n.add(d) | |
| 494 | fi | |
| 495 | od | |
| 496 | ||
| 497 | _diagnostics = n | |
| 498 | ||
| 499 | let ni = LIST[INLAY](_inlays.count) | |
| 500 | ||
| 501 | for i in _inlays do | |
| 502 | if !(i.is_compile_expressions /\ span.contains(i.location)) then | |
| 503 | ni.add(i) | |
| 504 | fi | |
| 505 | od | |
| 506 | ||
| 507 | _inlays = ni | |
| 508 | ||
| 509 | _reseed_deduplicator() | |
| 510 | si | |
| 511 | ||
| 512 | clear_expression_diagnostics() is | |
| 513 | let n = LIST[DIAGNOSTIC_MESSAGE](_diagnostics.count) | |
| 514 | ||
| 515 | for d in _diagnostics do | |
| 516 | if !d.is_compile_expressions then | |
| 517 | n.add(d) | |
| 518 | fi | |
| 519 | od | |
| 520 | ||
| 521 | _diagnostics = n | |
| 522 | ||
| 523 | let ni = LIST[INLAY](_inlays.count) | |
| 524 | ||
| 525 | for i in _inlays do | |
| 526 | if !i.is_compile_expressions then | |
| 527 | ni.add(i) | |
| 528 | fi | |
| 529 | od | |
| 530 | ||
| 531 | _inlays = ni | |
| 532 | ||
| 533 | _reseed_deduplicator() | |
| 534 | si | |
| 535 | ||
| 536 | clear_inlays(code: string) is | |
| 537 | let n = LIST[INLAY](_inlays.count) | |
| 538 | ||
| 539 | for i in _inlays do | |
| 540 | let carrier_code = i.code | |
| 541 | ||
| 542 | if !(carrier_code? /\ carrier_code.starts_with(code)) then | |
| 543 | n.add(i) | |
| 544 | fi | |
| 545 | od | |
| 546 | ||
| 547 | _inlays = n | |
| 548 | si | |
| 549 | ||
| 550 | write_all_diagnostics(writer: TextWriter, formatter: DiagnosticFormatter) is | |
| 551 | let written_any mut = false | |
| 552 | ||
| 553 | for diagnostic in _diagnostics do | |
| 554 | let formatted = formatter.format(diagnostic) | |
| 555 | ||
| 556 | if formatted? then | |
| 557 | written_any = true | |
| 558 | writer.write(formatted) | |
| 559 | writer.write("\n") | |
| 560 | fi | |
| 561 | od | |
| 562 | ||
| 563 | if !written_any /\ formatter.need_clear_errors then | |
| 564 | // clear errors in the client | |
| 565 | writer.write(_path) | |
| 566 | writer.write("\n") | |
| 567 | fi | |
| 568 | si | |
| 569 | si | |
| 570 | ||
| 571 | class DIAGNOSTICS_STATE is | |
| 572 | // Off for a test run, where a snapshot has to show exactly | |
| 573 | // what the passes reported. | |
| 574 | deduplicate: bool public | |
| 575 | count: int => _diagnostics_by_source_path.values |> reduce(0, (r, d) => r + d.count) | |
| 576 | is_poisoned: bool => _diagnostics_by_source_path.values |> any(d => d.is_poisoned) | |
| 577 | has_consumed_error: bool | |
| 578 | has_consumed_any: bool | |
| 579 | any_errors: bool => _diagnostics_by_source_path.values |> any(d => d.any_errors) | |
| 580 | any_warnings: bool => _diagnostics_by_source_path.values |> any(d => d.any_warnings) | |
| 581 | ||
| 582 | paths_with_errors: Iterable[string] is | |
| 583 | let result = LIST[string]() | |
| 584 | ||
| 585 | for pair in _diagnostics_by_source_path do | |
| 586 | if pair.value.any_errors then | |
| 587 | result.add(pair.key) | |
| 588 | fi | |
| 589 | od | |
| 590 | ||
| 591 | return result | |
| 592 | si | |
| 593 | ||
| 594 | diagnostic_paths: Iterable[string] => _diagnostics_by_source_path.keys | |
| 595 | ||
| 596 | diagnostics_for(path: string) -> Iterable[DIAGNOSTIC_MESSAGE] is | |
| 597 | let list: DIAGNOSTICS_LIST mut | |
| 598 | ||
| 599 | if !_diagnostics_by_source_path.try_get_value(path, list ref) then | |
| 600 | return LIST[DIAGNOSTIC_MESSAGE]() | |
| 601 | fi | |
| 602 | ||
| 603 | return list.diagnostics | |
| 604 | si | |
| 605 | ||
| 606 | _diagnostics_by_source_path: MutableMap[string, DIAGNOSTICS_LIST] | |
| 607 | ||
| 608 | init(deduplicate: bool) is | |
| 609 | self.deduplicate = deduplicate | |
| 610 | _diagnostics_by_source_path = MAP[string, DIAGNOSTICS_LIST]() | |
| 611 | si | |
| 612 | ||
| 613 | clear() is | |
| 614 | for i in _diagnostics_by_source_path.values do | |
| 615 | i.clear(false) | |
| 616 | od | |
| 617 | ||
| 618 | init(deduplicate) | |
| 619 | si | |
| 620 | ||
| 621 | clear(source_path: string, analysis_only: bool) is | |
| 622 | get_diagnostics_list(source_path).clear(analysis_only) | |
| 623 | si | |
| 624 | ||
| 625 | clear_global_declaration_diagnostics(source_path: string) is | |
| 626 | get_diagnostics_list(source_path).clear_global_declaration_diagnostics() | |
| 627 | si | |
| 628 | ||
| 629 | clear_expression_diagnostics(source_path: string) is | |
| 630 | get_diagnostics_list(source_path).clear_expression_diagnostics() | |
| 631 | si | |
| 632 | ||
| 633 | clear_expression_diagnostics_within(source_path: string, span: LOCATION) is | |
| 634 | get_diagnostics_list(source_path).clear_expression_diagnostics_within(span) | |
| 635 | si | |
| 636 | ||
| 637 | clear_inlays(source_path: string, code: string) is | |
| 638 | get_diagnostics_list(source_path).clear_inlays(code) | |
| 639 | si | |
| 640 | ||
| 641 | ||
| 642 | ||
| 643 | get_diagnostics_list(source_path: string) -> DIAGNOSTICS_LIST is | |
| 644 | let diagnostics_for_path: DIAGNOSTICS_LIST mut | |
| 645 | ||
| 646 | if !_diagnostics_by_source_path.try_get_value(source_path, diagnostics_for_path ref) then | |
| 647 | diagnostics_for_path = DIAGNOSTICS_LIST(source_path, deduplicate) | |
| 648 | ||
| 649 | _diagnostics_by_source_path.add(source_path, diagnostics_for_path) | |
| 650 | fi | |
| 651 | ||
| 652 | return diagnostics_for_path | |
| 653 | si | |
| 654 | ||
| 655 | add_diagnostic_message(source_path: string, message: DIAGNOSTIC_MESSAGE) is | |
| 656 | get_diagnostics_list(source_path).add(message) | |
| 657 | si | |
| 658 | ||
| 659 | add_inlay(source_path: string, inlay: INLAY) is | |
| 660 | get_diagnostics_list(source_path).add_inlay(inlay) | |
| 661 | si | |
| 662 | ||
| 663 | add_inferred_terminators(source_path: string, positions: Iterable[int]) is | |
| 664 | get_diagnostics_list(source_path).add_inferred_terminators(positions) | |
| 665 | si | |
| 666 | ||
| 667 | mark_poisoned(source_path: string) is | |
| 668 | get_diagnostics_list(source_path).mark_poisoned() | |
| 669 | si | |
| 670 | ||
| 671 | mark_consumed_error() is | |
| 672 | has_consumed_error = true | |
| 673 | si | |
| 674 | ||
| 675 | mark_consumed_any() is | |
| 676 | has_consumed_any = true | |
| 677 | si | |
| 678 | ||
| 679 | clear_consumed_error() is | |
| 680 | for i in _diagnostics_by_source_path.values do | |
| 681 | i.clear_consumed_error() | |
| 682 | od | |
| 683 | si | |
| 684 | ||
| 685 | clear_consumed_any() is | |
| 686 | for i in _diagnostics_by_source_path.values do | |
| 687 | i.clear_consumed_any() | |
| 688 | od | |
| 689 | si | |
| 690 | ||
| 691 | merge(state: DIAGNOSTICS_STATE) is | |
| 692 | for i in state._diagnostics_by_source_path do | |
| 693 | add_diagnostic_messages(i.key, i.value.diagnostics) | |
| 694 | add_inlays(i.key, i.value.inlays) | |
| 695 | add_inferred_terminators(i.key, i.value.inferred_terminators) | |
| 696 | ||
| 697 | if i.value.is_poisoned then | |
| 698 | mark_poisoned(i.key) | |
| 699 | fi | |
| 700 | od | |
| 701 | ||
| 702 | if state.has_consumed_error then | |
| 703 | mark_consumed_error() | |
| 704 | fi | |
| 705 | ||
| 706 | if state.has_consumed_any then | |
| 707 | mark_consumed_any() | |
| 708 | fi | |
| 709 | si | |
| 710 | ||
| 711 | add_diagnostic_messages(source_path: string, diagnostics: Iterable[DIAGNOSTIC_MESSAGE]) is | |
| 712 | let list = get_diagnostics_list(source_path) | |
| 713 | ||
| 714 | for d in diagnostics do | |
| 715 | list.add(d) | |
| 716 | od | |
| 717 | si | |
| 718 | ||
| 719 | add_inlays(source_path: string, inlays: Iterable[INLAY]) is | |
| 720 | let list = get_diagnostics_list(source_path) | |
| 721 | ||
| 722 | for i in inlays do | |
| 723 | list.add_inlay(i) | |
| 724 | od | |
| 725 | si | |
| 726 | ||
| 727 | write_all_diagnostics(writer: TextWriter, formatter: DiagnosticFormatter) is | |
| 728 | for list in _diagnostics_by_source_path.values do | |
| 729 | list.write_all_diagnostics(writer, formatter) | |
| 730 | od | |
| 731 | si | |
| 732 | si | |
| 733 | ||
| 734 | class DIAGNOSTICS_STORE: Logger is | |
| 735 | _states: STACK[DIAGNOSTICS_STATE] | |
| 736 | // The bottom, never-speculated diagnostics state. Lexer errors are | |
| 737 | // written straight here (see lexer_error). | |
| 738 | _base_state: DIAGNOSTICS_STATE | |
| 739 | _suppressed_codes: Collections.SET[string] | |
| 740 | _suppression_regions: SUPPRESSION_REGIONS | |
| 741 | _all_warnings_are_errors: bool | |
| 742 | _error_codes: Collections.SET[string] | |
| 743 | _hint_codes: Collections.SET[string] | |
| 744 | _info_codes: Collections.SET[string] | |
| 745 | _enabled_inlay_kinds: Collections.SET[string] | |
| 746 | ||
| 747 | is_poisoned: bool => _states.peek().is_poisoned | |
| 748 | has_consumed_error: bool => _states.peek().has_consumed_error | |
| 749 | has_consumed_any: bool => _states.peek().has_consumed_any | |
| 750 | ||
| 751 | error_count: int => _states.peek().count | |
| 752 | any_errors: bool => _states.peek().any_errors | |
| 753 | any_warnings: bool => _states.peek().any_warnings | |
| 754 | paths_with_errors: Iterable[string] => _states.peek().paths_with_errors | |
| 755 | diagnostic_paths: Iterable[string] => _states.peek().diagnostic_paths | |
| 756 | ||
| 757 | diagnostics_for(path: string) -> Iterable[DIAGNOSTIC_MESSAGE] => | |
| 758 | _states.peek().diagnostics_for(path) | |
| 759 | ||
| 760 | is_clean: bool => !any_errors /\ !has_consumed_error /\ !has_consumed_any | |
| 761 | ||
| 762 | is_analysis: bool public | |
| 763 | is_compile_expressions: bool public | |
| 764 | ||
| 765 | _open_files: Collections.SET[string] | |
| 766 | ||
| 767 | depth: int => _states.count | |
| 768 | ||
| 769 | // Off for a test run, so a captured snapshot shows exactly what | |
| 770 | // the passes reported and a pass that reports four times is | |
| 771 | // visible as four lines rather than hidden. | |
| 772 | _deduplicate: bool | |
| 773 | ||
| 774 | deduplicate: bool => _deduplicate | |
| 775 | ||
| 776 | keep_duplicate_diagnostics() is | |
| 777 | _deduplicate = false | |
| 778 | _base_state.deduplicate = false | |
| 779 | si | |
| 780 | ||
| 781 | init() is | |
| 782 | _states = STACK() | |
| 783 | _deduplicate = true | |
| 784 | _base_state = DIAGNOSTICS_STATE(_deduplicate) | |
| 785 | _states.push(_base_state) | |
| 786 | _suppressed_codes = Collections.SET[string]() | |
| 787 | ||
| 788 | // Suppressed by default: writing statement terminators out is | |
| 789 | // a style choice rather than a defect, so the warning is on | |
| 790 | // only when a project asks with `--warn redundant-semicolon`. | |
| 791 | _suppressed_codes.add("redundant-semicolon") | |
| 792 | ||
| 793 | _suppression_regions = SUPPRESSION_REGIONS() | |
| 794 | _error_codes = Collections.SET[string]() | |
| 795 | _hint_codes = Collections.SET[string]() | |
| 796 | _info_codes = Collections.SET[string]() | |
| 797 | _open_files = Collections.SET[string]() | |
| 798 | ||
| 799 | // On by default: the sparse kinds. The terminator kind (one | |
| 800 | // hint per inferred statement boundary) is off unless a | |
| 801 | // client asks with `--inlay terminator`. | |
| 802 | _enabled_inlay_kinds = Collections.SET[string]() | |
| 803 | _enabled_inlay_kinds.add(INLAY_KINDS.NARROWING) | |
| 804 | _enabled_inlay_kinds.add(INLAY_KINDS.DEFINITION_VIRTUALITY) | |
| 805 | si | |
| 806 | ||
| 807 | enable_inlay_kind(kind: string) is | |
| 808 | _enabled_inlay_kinds.add(kind) | |
| 809 | si | |
| 810 | ||
| 811 | disable_inlay_kind(kind: string) is | |
| 812 | _enabled_inlay_kinds.remove(kind) | |
| 813 | si | |
| 814 | ||
| 815 | is_inlay_kind_enabled(kind: string) -> bool => | |
| 816 | _enabled_inlay_kinds.contains(kind) | |
| 817 | ||
| 818 | // Returns the paths whose open state changed - opened or closed. | |
| 819 | // Only those files' hint gating moves, so only their expression | |
| 820 | // walks have anything to redo. | |
| 821 | set_open_files(paths: Collections.Iterable[string]) -> Collections.List[string] is | |
| 822 | let previous = _open_files | |
| 823 | _open_files = Collections.SET[string]() | |
| 824 | ||
| 825 | let changed = Collections.LIST[string]() | |
| 826 | ||
| 827 | for path in paths do | |
| 828 | _open_files.add(path) | |
| 829 | ||
| 830 | if !previous.contains(path) then | |
| 831 | changed.add(path) | |
| 832 | fi | |
| 833 | od | |
| 834 | ||
| 835 | for path in previous do | |
| 836 | if !_open_files.contains(path) then | |
| 837 | changed.add(path) | |
| 838 | fi | |
| 839 | od | |
| 840 | ||
| 841 | return changed | |
| 842 | si | |
| 843 | ||
| 844 | is_file_open(path: string?) -> bool => path? /\ _open_files.contains(path) | |
| 845 | ||
| 846 | suppress(code: string) is | |
| 847 | _suppressed_codes.add(code) | |
| 848 | si | |
| 849 | ||
| 850 | unsuppress(code: string) is | |
| 851 | _suppressed_codes.remove(code) | |
| 852 | si | |
| 853 | ||
| 854 | is_suppressed(code: string?) -> bool => | |
| 855 | code? /\ _suppressed_codes.contains(code) | |
| 856 | ||
| 857 | set_all_warnings_are_errors(value: bool) is | |
| 858 | _all_warnings_are_errors = value | |
| 859 | si | |
| 860 | ||
| 861 | promote_to_error(code: string) is | |
| 862 | _error_codes.add(code) | |
| 863 | si | |
| 864 | ||
| 865 | is_promoted_to_error(code: string?) -> bool => | |
| 866 | _all_warnings_are_errors \/ (code? /\ _error_codes.contains(code)) | |
| 867 | ||
| 868 | demote_to_hint(code: string) is | |
| 869 | _hint_codes.add(code) | |
| 870 | si | |
| 871 | ||
| 872 | is_demoted_to_hint(code: string?) -> bool => | |
| 873 | code? /\ _hint_codes.contains(code) | |
| 874 | ||
| 875 | demote_to_info(code: string) is | |
| 876 | _info_codes.add(code) | |
| 877 | si | |
| 878 | ||
| 879 | is_demoted_to_info(code: string?) -> bool => | |
| 880 | code? /\ _info_codes.contains(code) | |
| 881 | ||
| 882 | want_hint_for(location: LOCATION?) -> bool => | |
| 883 | is_analysis /\ location? /\ is_file_open(location.file_name) | |
| 884 | ||
| 885 | register_suppression_region(region: LOCATION, code: string) is | |
| 886 | _suppression_regions.register(region, code) | |
| 887 | si | |
| 888 | ||
| 889 | clear_suppression_regions(path: string) is | |
| 890 | _suppression_regions.clear(path) | |
| 891 | si | |
| 892 | ||
| 893 | clear_suppression_regions() is | |
| 894 | _suppression_regions.clear() | |
| 895 | si | |
| 896 | ||
| 897 | is_suppressed(code: string?, location: LOCATION) -> bool => | |
| 898 | code? /\ | |
| 899 | (_suppressed_codes.contains(code) \/ _suppression_regions.contains(code, location)) | |
| 900 | ||
| 901 | start_analysis() is | |
| 902 | is_analysis = true | |
| 903 | si | |
| 904 | ||
| 905 | end_analysis() is | |
| 906 | is_analysis = false | |
| 907 | is_compile_expressions = false | |
| 908 | si | |
| 909 | ||
| 910 | set_is_compiling_expressions(value: bool) is | |
| 911 | is_compile_expressions = value | |
| 912 | si | |
| 913 | ||
| 914 | only: DIAGNOSTICS_STATE is | |
| 915 | assert _states.count == 1 else "expected exactly one stacked diagnostics state, found {_states.count}" | |
| 916 | ||
| 917 | return _states.peek() | |
| 918 | si | |
| 919 | ||
| 920 | top: DIAGNOSTICS_STATE is | |
| 921 | assert _states.count >= 1 else "expected at least one stacked diagnostics state" | |
| 922 | ||
| 923 | return _states.peek() | |
| 924 | si | |
| 925 | ||
| 926 | pop() -> DIAGNOSTICS_STATE is | |
| 927 | assert _states.count >= 1 else "expected at least one stacked diagnostics state" | |
| 928 | ||
| 929 | return _states.pop() | |
| 930 | si | |
| 931 | ||
| 932 | clear(source_path: string, analysis_only: bool) is | |
| 933 | only.clear(source_path, analysis_only) | |
| 934 | si | |
| 935 | ||
| 936 | clear_global_declaration_diagnostics(source_path: string) is | |
| 937 | only.clear_global_declaration_diagnostics(source_path) | |
| 938 | si | |
| 939 | ||
| 940 | clear_expression_diagnostics(source_path: string) is | |
| 941 | only.clear_expression_diagnostics(source_path) | |
| 942 | si | |
| 943 | ||
| 944 | clear_expression_diagnostics_within(source_path: string, span: LOCATION) is | |
| 945 | only.clear_expression_diagnostics_within(source_path, span) | |
| 946 | si | |
| 947 | ||
| 948 | clear_inlays(source_path: string, code: string) is | |
| 949 | only.clear_inlays(source_path, code) | |
| 950 | si | |
| 951 | ||
| 952 | ||
| 953 | speculate() is | |
| 954 | _states.push(DIAGNOSTICS_STATE(_deduplicate)) | |
| 955 | si | |
| 956 | ||
| 957 | roll_back() -> DIAGNOSTICS_STATE => | |
| 958 | pop() | |
| 959 | ||
| 960 | commit() is | |
| 961 | let to_merge = pop() | |
| 962 | ||
| 963 | top.merge(to_merge) | |
| 964 | si | |
| 965 | ||
| 966 | mark() -> int => _states.count | |
| 967 | release(mark: int) is | |
| 968 | while _states.count > mark do | |
| 969 | pop() | |
| 970 | od | |
| 971 | si | |
| 972 | ||
| 973 | speculate_then_commit() -> LOGGER_SPECULATE_THEN_COMMIT => | |
| 974 | LOGGER_SPECULATE_THEN_COMMIT(self) | |
| 975 | ||
| 976 | speculate_then_backtrack() -> LOGGER_SPECULATE_THEN_BACKTRACK => | |
| 977 | LOGGER_SPECULATE_THEN_BACKTRACK(self) | |
| 978 | ||
| 979 | mark_then_release() -> MARK_THEN_RELEASE => | |
| 980 | MARK_THEN_RELEASE(self) | |
| 981 | ||
| 982 | merge(state: DIAGNOSTICS_STATE) is | |
| 983 | top.merge(state) | |
| 984 | si | |
| 985 | ||
| 986 | write_all_diagnostics(writer: TextWriter, formatter: DiagnosticFormatter) is | |
| 987 | only.write_all_diagnostics(writer, formatter) | |
| 988 | si | |
| 989 | ||
| 990 | poison(location: LOCATION) is | |
| 991 | top.mark_poisoned(location.file_name) | |
| 992 | si | |
| 993 | ||
| 994 | mark_consumed_error() is | |
| 995 | top.mark_consumed_error() | |
| 996 | si | |
| 997 | ||
| 998 | mark_consumed_any() is | |
| 999 | top.mark_consumed_any() | |
| 1000 | si | |
| 1001 | ||
| 1002 | mark_consumed_any_if(consumed: bool) is | |
| 1003 | if consumed then | |
| 1004 | top.mark_consumed_any() | |
| 1005 | fi | |
| 1006 | si | |
| 1007 | ||
| 1008 | clear_consumed_any() is | |
| 1009 | top.clear_consumed_any() | |
| 1010 | si | |
| 1011 | ||
| 1012 | clear_consumed_error() is | |
| 1013 | top.clear_consumed_error() | |
| 1014 | si | |
| 1015 | ||
| 1016 | exception(location: LOCATION, exception: Exception, message: string) is | |
| 1017 | debug_always("{location} exception: {exception.to_string().replace_line_endings(" ")}") | |
| 1018 | ||
| 1019 | if _states.count == 0 then | |
| 1020 | debug_always("diagnostics store: exception depth {_states.count}: {location}: {message}: {exception}") | |
| 1021 | fi | |
| 1022 | ||
| 1023 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.EXCEPTION, location, "{message}: {exception}")) | |
| 1024 | si | |
| 1025 | ||
| 1026 | exception(mark: int, location: LOCATION, exception: Exception, message: string) is | |
| 1027 | release(mark) | |
| 1028 | ||
| 1029 | self.exception(location, exception, message) | |
| 1030 | si | |
| 1031 | ||
| 1032 | fatal(location: LOCATION, message: string) is | |
| 1033 | debug_always("{location}: fatal: {message}") | |
| 1034 | ||
| 1035 | debug_always("diagnostics store: fatal depth {_states.count}: {location}: {message}") | |
| 1036 | ||
| 1037 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.FATAL, location, message)) | |
| 1038 | si | |
| 1039 | ||
| 1040 | error(location: LOCATION, message: string) is | |
| 1041 | if let listen = DIAGNOSTIC_TRACE.on_error then | |
| 1042 | listen(location, message) | |
| 1043 | fi | |
| 1044 | ||
| 1045 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.ERROR, location, message)) | |
| 1046 | si | |
| 1047 | ||
| 1048 | error(location: LOCATION, message: string, related_location: LOCATION, related_message: string) is | |
| 1049 | let related = LIST[RELATED_LOCATION]() | |
| 1050 | ||
| 1051 | related.add(RELATED_LOCATION(related_location, related_message)) | |
| 1052 | ||
| 1053 | error(location, message, related) | |
| 1054 | si | |
| 1055 | ||
| 1056 | error(location: LOCATION, message: string, related: LIST[RELATED_LOCATION]) is | |
| 1057 | if let usable = _usable_related(related) then | |
| 1058 | top.add_diagnostic_message( | |
| 1059 | location.file_name, | |
| 1060 | DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.ERROR, location, null, message, usable) | |
| 1061 | ) | |
| 1062 | else | |
| 1063 | error(location, message) | |
| 1064 | fi | |
| 1065 | si | |
| 1066 | ||
| 1067 | // A related location sitting on a sentinel ("internal", "unknown", | |
| 1068 | // "reflected") | |
| 1069 | // is not a jump target - there is no file behind it - so it is | |
| 1070 | // dropped rather than rendered as noise. Returns null when nothing | |
| 1071 | // usable is left, so the caller falls back to the related-free form. | |
| 1072 | _usable_related(related: LIST[RELATED_LOCATION]) -> LIST[RELATED_LOCATION]? is | |
| 1073 | let usable = LIST[RELATED_LOCATION](related.count) | |
| 1074 | ||
| 1075 | for r in related do | |
| 1076 | if !r.location.is_reflected /\ !r.location.is_unknown /\ !r.location.is_internal then | |
| 1077 | usable.add(r) | |
| 1078 | fi | |
| 1079 | od | |
| 1080 | ||
| 1081 | if usable.count == 0 then | |
| 1082 | return null | |
| 1083 | fi | |
| 1084 | ||
| 1085 | return usable | |
| 1086 | si | |
| 1087 | ||
| 1088 | // A lexical error comes from the tokenizer, which reads each source | |
| 1089 | // position exactly once — parser speculation replays buffered tokens, | |
| 1090 | // it never re-lexes. So a lexer error is produced once no matter how | |
| 1091 | // the parser speculates over those tokens. It is written to the base | |
| 1092 | // state rather than the current speculative one, so a parse that reads | |
| 1093 | // the tokens speculatively and then backtracks does not roll it back | |
| 1094 | // and lose it. Unlike a parse error — which the parser regenerates on | |
| 1095 | // every re-walk and so must roll back to avoid duplicates — a lexer | |
| 1096 | // error cannot duplicate, because nothing regenerates it. | |
| 1097 | lexer_error(location: LOCATION, message: string) is | |
| 1098 | environment_error(location, message) | |
| 1099 | si | |
| 1100 | ||
| 1101 | environment_error(location: LOCATION, message: string) is | |
| 1102 | _base_state.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.ERROR, location, message)) | |
| 1103 | si | |
| 1104 | ||
| 1105 | warn(location: LOCATION, message: string) is | |
| 1106 | let severity = | |
| 1107 | if _all_warnings_are_errors then | |
| 1108 | DiagnosticSeverity.ERROR | |
| 1109 | else | |
| 1110 | DiagnosticSeverity.WARN | |
| 1111 | fi | |
| 1112 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, severity, location, message)) | |
| 1113 | si | |
| 1114 | ||
| 1115 | warn(location: LOCATION, code: string, message: string) is | |
| 1116 | if is_suppressed(code, location) then | |
| 1117 | return | |
| 1118 | fi | |
| 1119 | ||
| 1120 | if is_demoted_to_hint(code) then | |
| 1121 | // Emitted as an editor-only hint under the same gate as a | |
| 1122 | // native hint: dropped in batch compilation, surfaced in | |
| 1123 | // analysis mode only for a file the client has open. | |
| 1124 | if !want_hint_for(location) then | |
| 1125 | return | |
| 1126 | fi | |
| 1127 | ||
| 1128 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.HINT, location, code, message)) | |
| 1129 | ||
| 1130 | return | |
| 1131 | fi | |
| 1132 | ||
| 1133 | if is_demoted_to_info(code) then | |
| 1134 | // Unlike a hint, info stays a normal batch-visible | |
| 1135 | // diagnostic; it is not subject to the editor-only gate. | |
| 1136 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.INFO, location, code, message)) | |
| 1137 | ||
| 1138 | return | |
| 1139 | fi | |
| 1140 | ||
| 1141 | let severity = | |
| 1142 | if is_promoted_to_error(code) then | |
| 1143 | DiagnosticSeverity.ERROR | |
| 1144 | else | |
| 1145 | DiagnosticSeverity.WARN | |
| 1146 | fi | |
| 1147 | ||
| 1148 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, severity, location, code, message)) | |
| 1149 | si | |
| 1150 | ||
| 1151 | warn(location: LOCATION, code: string, message: string, related_location: LOCATION, related_message: string) is | |
| 1152 | let related = LIST[RELATED_LOCATION]() | |
| 1153 | ||
| 1154 | related.add(RELATED_LOCATION(related_location, related_message)) | |
| 1155 | ||
| 1156 | warn(location, code, message, related) | |
| 1157 | si | |
| 1158 | ||
| 1159 | warn(location: LOCATION, code: string, message: string, related: LIST[RELATED_LOCATION]) is | |
| 1160 | if is_suppressed(code, location) then | |
| 1161 | // Suppressed: the plain coded route drops it, exactly as | |
| 1162 | // it would without a related location. | |
| 1163 | warn(location, code, message) | |
| 1164 | ||
| 1165 | return | |
| 1166 | fi | |
| 1167 | ||
| 1168 | if is_demoted_to_hint(code) then | |
| 1169 | warn(location, code, message) | |
| 1170 | ||
| 1171 | return | |
| 1172 | fi | |
| 1173 | ||
| 1174 | if is_demoted_to_info(code) then | |
| 1175 | // Info is a normal batch-visible diagnostic, so its | |
| 1176 | // related locations survive the demotion - the reader | |
| 1177 | // still needs the advice and the slug to suppress by. | |
| 1178 | top.add_diagnostic_message( | |
| 1179 | location.file_name, | |
| 1180 | DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.INFO, location, code, message, _usable_related(related)) | |
| 1181 | ) | |
| 1182 | ||
| 1183 | return | |
| 1184 | fi | |
| 1185 | ||
| 1186 | let usable = _usable_related(related) | |
| 1187 | ||
| 1188 | if !usable? then | |
| 1189 | warn(location, code, message) | |
| 1190 | ||
| 1191 | return | |
| 1192 | fi | |
| 1193 | ||
| 1194 | let severity = | |
| 1195 | if is_promoted_to_error(code) then | |
| 1196 | DiagnosticSeverity.ERROR | |
| 1197 | else | |
| 1198 | DiagnosticSeverity.WARN | |
| 1199 | fi | |
| 1200 | ||
| 1201 | top.add_diagnostic_message( | |
| 1202 | location.file_name, | |
| 1203 | DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, severity, location, code, message, usable) | |
| 1204 | ) | |
| 1205 | si | |
| 1206 | ||
| 1207 | info(location: LOCATION, message: string) is | |
| 1208 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.INFO, location, message)) | |
| 1209 | si | |
| 1210 | ||
| 1211 | info(location: LOCATION, code: string, message: string) is | |
| 1212 | if is_suppressed(code, location) then | |
| 1213 | return | |
| 1214 | fi | |
| 1215 | ||
| 1216 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.INFO, location, code, message)) | |
| 1217 | si | |
| 1218 | ||
| 1219 | hint(location: LOCATION, message: string) is | |
| 1220 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.HINT, location, message)) | |
| 1221 | si | |
| 1222 | ||
| 1223 | hint(location: LOCATION, code: string, message: string) is | |
| 1224 | if is_suppressed(code, location) then | |
| 1225 | return | |
| 1226 | fi | |
| 1227 | ||
| 1228 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.HINT, location, code, message)) | |
| 1229 | si | |
| 1230 | ||
| 1231 | hint(location: LOCATION, code: string, message: string, related_location: LOCATION, related_message: string) is | |
| 1232 | if is_suppressed(code, location) then | |
| 1233 | return | |
| 1234 | fi | |
| 1235 | ||
| 1236 | let related = LIST[RELATED_LOCATION]() | |
| 1237 | ||
| 1238 | related.add(RELATED_LOCATION(related_location, related_message)) | |
| 1239 | ||
| 1240 | let usable = _usable_related(related) | |
| 1241 | ||
| 1242 | if !usable? then | |
| 1243 | hint(location, code, message) | |
| 1244 | ||
| 1245 | return | |
| 1246 | fi | |
| 1247 | ||
| 1248 | top.add_diagnostic_message( | |
| 1249 | location.file_name, | |
| 1250 | DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.HINT, location, code, message, usable) | |
| 1251 | ) | |
| 1252 | si | |
| 1253 | ||
| 1254 | inlay(location: LOCATION, code: string, label: string, detail: string?) is | |
| 1255 | if is_suppressed(code, location) then | |
| 1256 | return | |
| 1257 | fi | |
| 1258 | ||
| 1259 | // A disabled kind is never recorded, so it costs nothing to | |
| 1260 | // collect or serve. | |
| 1261 | let kind = INLAY_KINDS.for_code(code) | |
| 1262 | ||
| 1263 | if kind? /\ !_enabled_inlay_kinds.contains(kind) then | |
| 1264 | return | |
| 1265 | fi | |
| 1266 | ||
| 1267 | top.add_inlay(location.file_name, INLAY(is_analysis, is_compile_expressions, location, code, label, detail)) | |
| 1268 | si | |
| 1269 | ||
| 1270 | inlays_for(path: string) -> Collections.Iterable[INLAY] => | |
| 1271 | top.get_diagnostics_list(path).inlays | |
| 1272 | ||
| 1273 | // Positions of the statement boundaries the parser inferred in | |
| 1274 | // `path`, as LOCATION-packed ints in source order. Served by the | |
| 1275 | // INLAY_HINTS handler when the terminator kind is enabled. | |
| 1276 | inferred_terminators_for(path: string) -> Collections.List[int] => | |
| 1277 | top.get_diagnostics_list(path).inferred_terminators | |
| 1278 | ||
| 1279 | // LOCATION packs a position as (line << 12) | column, so a column | |
| 1280 | // beyond this would carry into the line. | |
| 1281 | _MAX_PACKED_COLUMN: int static => 0xFFF | |
| 1282 | ||
| 1283 | note_inferred_terminator(location: LOCATION) is | |
| 1284 | if !is_inlay_kind_enabled(INLAY_KINDS.TERMINATOR) then | |
| 1285 | return | |
| 1286 | fi | |
| 1287 | ||
| 1288 | // The hint sits where the `;` would have gone: one past the | |
| 1289 | // end of the construct being terminated. A LOCATION's end | |
| 1290 | // column is inclusive - it is the construct's last character, | |
| 1291 | // which is why every diagnostic on the wire sends end_column + 1 | |
| 1292 | // - so the position is one to the right of it. Capped at the | |
| 1293 | // 12 bits the packed pair holds. | |
| 1294 | let column = System.Math.min(location.end_column + 1, _MAX_PACKED_COLUMN) | |
| 1295 | ||
| 1296 | top.get_diagnostics_list(location.file_name) | |
| 1297 | .add_inferred_terminators([LOCATION.pair(location.end_line, column)]) | |
| 1298 | si | |
| 1299 | ||
| 1300 | poison(location: LOCATION, message: string) is | |
| 1301 | top.add_diagnostic_message(location.file_name, DIAGNOSTIC_MESSAGE(is_analysis, is_compile_expressions, DiagnosticSeverity.FATAL, location, message)) | |
| 1302 | top.mark_poisoned(location.file_name) | |
| 1303 | si | |
| 1304 | ||
| 1305 | write_poison_messages() is | |
| 1306 | if is_poisoned then | |
| 1307 | debug_always("internal compiler error") | |
| 1308 | IO.Std.error.flush() | |
| 1309 | fi | |
| 1310 | si | |
| 1311 | si | |
| 1312 | si |