Skip to content
← Back

src/logging/diagnostics_store.ghul

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