1use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt;
6use std::ops::Range;
7
8use super::{Grammar, GrammarExpr, GrammarRule};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct RuleSpan {
13 pub rule: String,
15 pub span: Option<Range<usize>>,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Severity {
22 Error,
24 Warning,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum DiagnosticKind {
31 UndefinedNonTerminal {
33 name: String,
35 referenced_in: String,
37 },
38 LeftRecursion {
40 cycle: Vec<String>,
42 },
43 UnreachableRule {
45 name: String,
47 },
48 NullableRepetition {
50 rule: String,
52 detail: String,
54 },
55 DuplicateRule {
57 name: String,
59 },
60 UnusedCapture {
62 rule: String,
64 label: String,
66 },
67}
68
69#[derive(Clone, Debug, PartialEq, Eq)]
71pub struct GrammarDiagnostic {
72 pub kind: DiagnosticKind,
74 pub severity: Severity,
76 pub message: String,
78 pub location: RuleSpan,
80}
81
82impl GrammarDiagnostic {
83 #[must_use]
85 pub fn is_error(&self) -> bool {
86 self.severity == Severity::Error
87 }
88}
89
90impl fmt::Display for GrammarDiagnostic {
91 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92 formatter.write_str(&self.message)
93 }
94}
95
96impl Error for GrammarDiagnostic {}
97
98impl Grammar {
99 #[must_use]
101 pub fn validate(&self) -> Vec<GrammarDiagnostic> {
102 validate(self)
103 }
104}
105
106#[must_use]
108pub fn validate(grammar: &Grammar) -> Vec<GrammarDiagnostic> {
109 let defined_names = defined_rule_names(grammar);
110 let nullability = compute_nullability(grammar);
111
112 let mut diagnostics = Vec::new();
113 diagnostics.extend(check_duplicate_rules(grammar));
114 diagnostics.extend(check_undefined_nonterminals(grammar, &defined_names));
115 diagnostics.extend(check_left_recursion(grammar, &nullability));
116 diagnostics.extend(check_unreachable_rules(grammar));
117 diagnostics.extend(check_nullable_repetition(grammar, &nullability));
118 diagnostics.extend(check_unused_captures(grammar));
119
120 sort_diagnostics(grammar, &mut diagnostics);
121 diagnostics
122}
123
124fn check_duplicate_rules(grammar: &Grammar) -> Vec<GrammarDiagnostic> {
125 let mut counts = BTreeMap::<String, usize>::new();
126 for rule in grammar.rules() {
127 *counts.entry(rule.name.clone()).or_default() += 1;
128 }
129
130 counts
131 .into_iter()
132 .filter(|(_, count)| *count > 1)
133 .map(|(name, _)| {
134 let message = format!(
135 "rule `{name}` is defined more than once; merge alternatives into one rule or rename the duplicate."
136 );
137 GrammarDiagnostic {
138 kind: DiagnosticKind::DuplicateRule { name: name.clone() },
139 severity: Severity::Error,
140 message,
141 location: rule_location(name),
142 }
143 })
144 .collect()
145}
146
147fn check_undefined_nonterminals(
148 grammar: &Grammar,
149 defined_names: &[String],
150) -> Vec<GrammarDiagnostic> {
151 let defined = defined_names
152 .iter()
153 .map(String::as_str)
154 .collect::<BTreeSet<_>>();
155 let mut diagnostics = Vec::new();
156
157 for rule in grammar.rules() {
158 for name in collect_nonterminals(rule.expr()) {
159 if defined.contains(name.as_str()) {
160 continue;
161 }
162
163 let suggestion = nearest_rule_name(&name, defined_names);
164 let message = suggestion.map_or_else(
165 || {
166 format!(
167 "rule `{}` references undefined non-terminal `{name}`; define it or fix the spelling.",
168 rule.name()
169 )
170 },
171 |candidate| {
172 format!(
173 "rule `{}` references undefined non-terminal `{name}`; did you mean `{candidate}`? Define it or fix the spelling.",
174 rule.name()
175 )
176 },
177 );
178 diagnostics.push(GrammarDiagnostic {
179 kind: DiagnosticKind::UndefinedNonTerminal {
180 name,
181 referenced_in: rule.name.clone(),
182 },
183 severity: Severity::Error,
184 message,
185 location: rule_location(rule.name()),
186 });
187 }
188 }
189
190 diagnostics
191}
192
193fn check_left_recursion(
194 grammar: &Grammar,
195 nullability: &BTreeMap<String, bool>,
196) -> Vec<GrammarDiagnostic> {
197 let graph = left_reference_graph(grammar, nullability);
198 let mut diagnostics = Vec::new();
199 let mut seen_cycles = BTreeSet::new();
200
201 for rule in grammar.rules() {
202 let mut path = vec![rule.name.clone()];
203 let mut seen_rules = BTreeSet::from([rule.name.clone()]);
204 collect_left_cycles(
205 rule.name(),
206 rule.name(),
207 &graph,
208 &mut seen_rules,
209 &mut path,
210 &mut seen_cycles,
211 &mut diagnostics,
212 );
213 }
214
215 diagnostics
216}
217
218fn collect_left_cycles(
219 target: &str,
220 current: &str,
221 graph: &BTreeMap<String, BTreeSet<String>>,
222 seen_rules: &mut BTreeSet<String>,
223 path: &mut Vec<String>,
224 seen_cycles: &mut BTreeSet<String>,
225 diagnostics: &mut Vec<GrammarDiagnostic>,
226) {
227 let Some(next_rules) = graph.get(current) else {
228 return;
229 };
230
231 for next in next_rules {
232 if next == target {
233 let mut cycle = path.clone();
234 cycle.push(target.to_string());
235 if seen_cycles.insert(canonical_cycle_key(&cycle)) {
236 let cycle_text = cycle.join(" -> ");
237 diagnostics.push(GrammarDiagnostic {
238 kind: DiagnosticKind::LeftRecursion {
239 cycle: cycle.clone(),
240 },
241 severity: Severity::Error,
242 message: format!(
243 "rule `{target}` is left-recursive (`{cycle_text}`); a recursive-descent/PEG parser may not terminate. Rewrite using repetition or factor the common prefix."
244 ),
245 location: rule_location(target),
246 });
247 }
248 } else if graph.contains_key(next) && seen_rules.insert(next.clone()) {
249 path.push(next.clone());
250 collect_left_cycles(
251 target,
252 next,
253 graph,
254 seen_rules,
255 path,
256 seen_cycles,
257 diagnostics,
258 );
259 path.pop();
260 seen_rules.remove(next);
261 }
262 }
263}
264
265fn check_unreachable_rules(grammar: &Grammar) -> Vec<GrammarDiagnostic> {
266 let Some(start_rule) = grammar.start_rule() else {
267 return Vec::new();
268 };
269
270 let defined = grammar
271 .rules()
272 .iter()
273 .map(GrammarRule::name)
274 .collect::<BTreeSet<_>>();
275 let mut reachable = BTreeSet::new();
276 let mut stack = vec![start_rule.name.clone()];
277
278 while let Some(name) = stack.pop() {
279 if !reachable.insert(name.clone()) {
280 continue;
281 }
282
283 if let Some(rule) = grammar.rule(&name) {
284 for reference in collect_nonterminals(rule.expr()) {
285 if defined.contains(reference.as_str()) {
286 stack.push(reference);
287 }
288 }
289 }
290 }
291
292 grammar
293 .rules()
294 .iter()
295 .filter(|rule| !reachable.contains(rule.name()))
296 .map(|rule| GrammarDiagnostic {
297 kind: DiagnosticKind::UnreachableRule {
298 name: rule.name.clone(),
299 },
300 severity: Severity::Warning,
301 message: format!(
302 "rule `{}` is not reachable from start rule `{}`; remove it or reference it from a reachable rule.",
303 rule.name(),
304 start_rule.name()
305 ),
306 location: rule_location(rule.name()),
307 })
308 .collect()
309}
310
311fn check_nullable_repetition(
312 grammar: &Grammar,
313 nullability: &BTreeMap<String, bool>,
314) -> Vec<GrammarDiagnostic> {
315 let suspicious_nullable = compute_suspicious_nullability(grammar, nullability);
316 let mut diagnostics = Vec::new();
317
318 for rule in grammar.rules() {
319 if suspicious_nullable
320 .get(rule.name())
321 .copied()
322 .unwrap_or(false)
323 {
324 diagnostics.push(GrammarDiagnostic {
325 kind: DiagnosticKind::NullableRepetition {
326 rule: rule.name.clone(),
327 detail: "rule body is nullable".to_string(),
328 },
329 severity: Severity::Warning,
330 message: format!(
331 "rule `{}` can match empty; add a required terminal/non-terminal or document the rule as intentional.",
332 rule.name()
333 ),
334 location: rule_location(rule.name()),
335 });
336 }
337 collect_nullable_repetitions(rule.name(), rule.expr(), nullability, &mut diagnostics);
338 }
339
340 diagnostics
341}
342
343fn collect_nullable_repetitions(
344 rule_name: &str,
345 expr: &GrammarExpr,
346 nullability: &BTreeMap<String, bool>,
347 diagnostics: &mut Vec<GrammarDiagnostic>,
348) {
349 match expr {
350 GrammarExpr::ZeroOrMore(inner) => {
351 push_nullable_repetition(
352 rule_name,
353 "zero-or-more repetition",
354 inner,
355 nullability,
356 diagnostics,
357 );
358 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
359 }
360 GrammarExpr::OneOrMore(inner) => {
361 push_nullable_repetition(
362 rule_name,
363 "one-or-more repetition",
364 inner,
365 nullability,
366 diagnostics,
367 );
368 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
369 }
370 GrammarExpr::Repeat { expr: inner, .. } => {
371 push_nullable_repetition(
372 rule_name,
373 "counted repetition",
374 inner,
375 nullability,
376 diagnostics,
377 );
378 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
379 }
380 GrammarExpr::Choice { alternatives, .. } => {
381 for alternative in alternatives {
382 collect_nullable_repetitions(rule_name, alternative, nullability, diagnostics);
383 }
384 }
385 GrammarExpr::Sequence(items) => {
386 for item in items {
387 collect_nullable_repetitions(rule_name, item, nullability, diagnostics);
388 }
389 }
390 GrammarExpr::Optional(inner)
391 | GrammarExpr::And(inner)
392 | GrammarExpr::Not(inner)
393 | GrammarExpr::Capture { expr: inner, .. } => {
394 collect_nullable_repetitions(rule_name, inner, nullability, diagnostics);
395 }
396 GrammarExpr::Empty
397 | GrammarExpr::Terminal(_)
398 | GrammarExpr::TerminalInsensitive(_)
399 | GrammarExpr::CharRange(_, _)
400 | GrammarExpr::CharClass { .. }
401 | GrammarExpr::AnyChar
402 | GrammarExpr::NonTerminal(_) => {}
403 }
404}
405
406fn push_nullable_repetition(
407 rule_name: &str,
408 repetition: &str,
409 inner: &GrammarExpr,
410 nullability: &BTreeMap<String, bool>,
411 diagnostics: &mut Vec<GrammarDiagnostic>,
412) {
413 if !expr_is_nullable(inner, nullability) {
414 return;
415 }
416
417 let detail = format!("{repetition} has nullable inner expression `{inner}`");
418 diagnostics.push(GrammarDiagnostic {
419 kind: DiagnosticKind::NullableRepetition {
420 rule: rule_name.to_string(),
421 detail: detail.clone(),
422 },
423 severity: Severity::Warning,
424 message: format!(
425 "rule `{rule_name}` uses a {detail}; make the repeated expression consume input or move the optional part outside the repetition."
426 ),
427 location: rule_location(rule_name),
428 });
429}
430
431fn check_unused_captures(grammar: &Grammar) -> Vec<GrammarDiagnostic> {
432 let mut diagnostics = Vec::new();
433 for rule in grammar.rules() {
434 collect_unused_captures(rule.name(), rule.expr(), &mut diagnostics);
435 }
436 diagnostics
437}
438
439fn collect_unused_captures(
440 rule_name: &str,
441 expr: &GrammarExpr,
442 diagnostics: &mut Vec<GrammarDiagnostic>,
443) {
444 match expr {
445 GrammarExpr::Capture {
446 label: Some(label),
447 expr,
448 } => {
449 diagnostics.push(GrammarDiagnostic {
450 kind: DiagnosticKind::UnusedCapture {
451 rule: rule_name.to_string(),
452 label: label.clone(),
453 },
454 severity: Severity::Warning,
455 message: format!(
456 "capture label `{label}` in rule `{rule_name}` is not used by grammar semantics; remove the label or wire it to a consumer."
457 ),
458 location: rule_location(rule_name),
459 });
460 collect_unused_captures(rule_name, expr, diagnostics);
461 }
462 GrammarExpr::Capture { label: None, expr }
463 | GrammarExpr::Optional(expr)
464 | GrammarExpr::ZeroOrMore(expr)
465 | GrammarExpr::OneOrMore(expr)
466 | GrammarExpr::And(expr)
467 | GrammarExpr::Not(expr)
468 | GrammarExpr::Repeat { expr, .. } => collect_unused_captures(rule_name, expr, diagnostics),
469 GrammarExpr::Choice { alternatives, .. } => {
470 for alternative in alternatives {
471 collect_unused_captures(rule_name, alternative, diagnostics);
472 }
473 }
474 GrammarExpr::Sequence(items) => {
475 for item in items {
476 collect_unused_captures(rule_name, item, diagnostics);
477 }
478 }
479 GrammarExpr::Empty
480 | GrammarExpr::Terminal(_)
481 | GrammarExpr::TerminalInsensitive(_)
482 | GrammarExpr::CharRange(_, _)
483 | GrammarExpr::CharClass { .. }
484 | GrammarExpr::AnyChar
485 | GrammarExpr::NonTerminal(_) => {}
486 }
487}
488
489fn left_reference_graph(
490 grammar: &Grammar,
491 nullability: &BTreeMap<String, bool>,
492) -> BTreeMap<String, BTreeSet<String>> {
493 let defined = grammar
494 .rules()
495 .iter()
496 .map(GrammarRule::name)
497 .collect::<BTreeSet<_>>();
498 let mut graph = BTreeMap::<String, BTreeSet<String>>::new();
499
500 for rule in grammar.rules() {
501 let mut references = BTreeSet::new();
502 collect_left_references(rule.expr(), nullability, &mut references);
503 graph.entry(rule.name.clone()).or_default().extend(
504 references
505 .into_iter()
506 .filter(|name| defined.contains(name.as_str())),
507 );
508 }
509
510 graph
511}
512
513fn collect_left_references(
514 expr: &GrammarExpr,
515 nullability: &BTreeMap<String, bool>,
516 references: &mut BTreeSet<String>,
517) {
518 match expr {
519 GrammarExpr::NonTerminal(name) => {
520 references.insert(name.clone());
521 }
522 GrammarExpr::Choice { alternatives, .. } => {
523 for alternative in alternatives {
524 collect_left_references(alternative, nullability, references);
525 }
526 }
527 GrammarExpr::Sequence(items) => {
528 for item in items {
529 collect_left_references(item, nullability, references);
530 if !expr_is_nullable(item, nullability) {
531 break;
532 }
533 }
534 }
535 GrammarExpr::Optional(expr)
536 | GrammarExpr::ZeroOrMore(expr)
537 | GrammarExpr::OneOrMore(expr)
538 | GrammarExpr::And(expr)
539 | GrammarExpr::Not(expr)
540 | GrammarExpr::Capture { expr, .. }
541 | GrammarExpr::Repeat { expr, .. } => {
542 collect_left_references(expr, nullability, references);
543 }
544 GrammarExpr::Empty
545 | GrammarExpr::Terminal(_)
546 | GrammarExpr::TerminalInsensitive(_)
547 | GrammarExpr::CharRange(_, _)
548 | GrammarExpr::CharClass { .. }
549 | GrammarExpr::AnyChar => {}
550 }
551}
552
553fn compute_nullability(grammar: &Grammar) -> BTreeMap<String, bool> {
554 let mut nullability = grammar
555 .rules()
556 .iter()
557 .map(|rule| (rule.name.clone(), false))
558 .collect::<BTreeMap<_, _>>();
559
560 loop {
561 let mut changed = false;
562 for rule in grammar.rules() {
563 if !expr_is_nullable(rule.expr(), &nullability) {
564 continue;
565 }
566
567 let entry = nullability.entry(rule.name.clone()).or_insert(false);
568 if !*entry {
569 *entry = true;
570 changed = true;
571 }
572 }
573
574 if !changed {
575 return nullability;
576 }
577 }
578}
579
580fn expr_is_nullable(expr: &GrammarExpr, nullability: &BTreeMap<String, bool>) -> bool {
581 match expr {
582 GrammarExpr::Terminal(value) | GrammarExpr::TerminalInsensitive(value) => value.is_empty(),
583 GrammarExpr::CharRange(_, _) | GrammarExpr::CharClass { .. } | GrammarExpr::AnyChar => {
584 false
585 }
586 GrammarExpr::NonTerminal(name) => nullability.get(name).copied().unwrap_or(false),
587 GrammarExpr::Choice { alternatives, .. } => alternatives
588 .iter()
589 .any(|alternative| expr_is_nullable(alternative, nullability)),
590 GrammarExpr::Sequence(items) => {
591 items.iter().all(|item| expr_is_nullable(item, nullability))
592 }
593 GrammarExpr::Empty
594 | GrammarExpr::Optional(_)
595 | GrammarExpr::ZeroOrMore(_)
596 | GrammarExpr::And(_)
597 | GrammarExpr::Not(_) => true,
598 GrammarExpr::OneOrMore(expr) | GrammarExpr::Capture { expr, .. } => {
599 expr_is_nullable(expr, nullability)
600 }
601 GrammarExpr::Repeat { expr, min, .. } => *min == 0 || expr_is_nullable(expr, nullability),
602 }
603}
604
605fn compute_suspicious_nullability(
606 grammar: &Grammar,
607 nullability: &BTreeMap<String, bool>,
608) -> BTreeMap<String, bool> {
609 let mut suspicious = grammar
610 .rules()
611 .iter()
612 .map(|rule| (rule.name.clone(), false))
613 .collect::<BTreeMap<_, _>>();
614
615 loop {
616 let mut changed = false;
617 for rule in grammar.rules() {
618 if !expr_is_suspicious_nullable(rule.expr(), nullability, &suspicious) {
619 continue;
620 }
621
622 let entry = suspicious.entry(rule.name.clone()).or_insert(false);
623 if !*entry {
624 *entry = true;
625 changed = true;
626 }
627 }
628
629 if !changed {
630 return suspicious;
631 }
632 }
633}
634
635fn expr_is_suspicious_nullable(
636 expr: &GrammarExpr,
637 nullability: &BTreeMap<String, bool>,
638 suspicious: &BTreeMap<String, bool>,
639) -> bool {
640 match expr {
641 GrammarExpr::Terminal(value) | GrammarExpr::TerminalInsensitive(value) => value.is_empty(),
642 GrammarExpr::CharRange(_, _) | GrammarExpr::CharClass { .. } | GrammarExpr::AnyChar => {
643 false
644 }
645 GrammarExpr::NonTerminal(name) => suspicious.get(name).copied().unwrap_or(false),
646 GrammarExpr::Choice { alternatives, .. } => alternatives
647 .iter()
648 .any(|alternative| expr_is_suspicious_nullable(alternative, nullability, suspicious)),
649 GrammarExpr::Sequence(items) => {
650 items.is_empty()
651 || (items.iter().all(|item| expr_is_nullable(item, nullability))
652 && items
653 .iter()
654 .any(|item| expr_is_suspicious_nullable(item, nullability, suspicious)))
655 }
656 GrammarExpr::Empty
657 | GrammarExpr::Optional(_)
658 | GrammarExpr::And(_)
659 | GrammarExpr::Not(_) => true,
660 GrammarExpr::ZeroOrMore(expr) | GrammarExpr::OneOrMore(expr) => {
661 expr_is_nullable(expr, nullability)
662 }
663 GrammarExpr::Repeat { expr, .. } => expr_is_nullable(expr, nullability),
664 GrammarExpr::Capture { expr, .. } => {
665 expr_is_suspicious_nullable(expr, nullability, suspicious)
666 }
667 }
668}
669
670fn collect_nonterminals(expr: &GrammarExpr) -> BTreeSet<String> {
671 let mut names = BTreeSet::new();
672 collect_nonterminals_into(expr, &mut names);
673 names
674}
675
676fn collect_nonterminals_into(expr: &GrammarExpr, names: &mut BTreeSet<String>) {
677 match expr {
678 GrammarExpr::NonTerminal(name) => {
679 names.insert(name.clone());
680 }
681 GrammarExpr::Choice { alternatives, .. } => {
682 for alternative in alternatives {
683 collect_nonterminals_into(alternative, names);
684 }
685 }
686 GrammarExpr::Sequence(items) => {
687 for item in items {
688 collect_nonterminals_into(item, names);
689 }
690 }
691 GrammarExpr::Optional(expr)
692 | GrammarExpr::ZeroOrMore(expr)
693 | GrammarExpr::OneOrMore(expr)
694 | GrammarExpr::And(expr)
695 | GrammarExpr::Not(expr)
696 | GrammarExpr::Capture { expr, .. }
697 | GrammarExpr::Repeat { expr, .. } => collect_nonterminals_into(expr, names),
698 GrammarExpr::Empty
699 | GrammarExpr::Terminal(_)
700 | GrammarExpr::TerminalInsensitive(_)
701 | GrammarExpr::CharRange(_, _)
702 | GrammarExpr::CharClass { .. }
703 | GrammarExpr::AnyChar => {}
704 }
705}
706
707fn defined_rule_names(grammar: &Grammar) -> Vec<String> {
708 let mut names = Vec::new();
709 let mut seen = BTreeSet::new();
710 for rule in grammar.rules() {
711 if seen.insert(rule.name.clone()) {
712 names.push(rule.name.clone());
713 }
714 }
715 names
716}
717
718fn nearest_rule_name<'a>(name: &str, candidates: &'a [String]) -> Option<&'a str> {
719 let mut best = None;
720
721 for candidate in candidates {
722 let distance = levenshtein(name, candidate);
723 if distance > 2 {
724 continue;
725 }
726
727 let should_replace = best.map_or(true, |(best_name, best_distance)| {
728 distance < best_distance
729 || (distance == best_distance && candidate.as_str() < best_name)
730 });
731 if should_replace {
732 best = Some((candidate.as_str(), distance));
733 }
734 }
735
736 best.map(|(candidate, _)| candidate)
737}
738
739fn levenshtein(left: &str, right: &str) -> usize {
740 let left_chars = left.chars().collect::<Vec<_>>();
741 let right_chars = right.chars().collect::<Vec<_>>();
742 let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
743 let mut current = vec![0; right_chars.len() + 1];
744
745 for (left_index, left_char) in left_chars.iter().enumerate() {
746 current[0] = left_index + 1;
747 for (right_index, right_char) in right_chars.iter().enumerate() {
748 let substitution_cost = usize::from(left_char != right_char);
749 current[right_index + 1] = (previous[right_index + 1] + 1)
750 .min(current[right_index] + 1)
751 .min(previous[right_index] + substitution_cost);
752 }
753 previous.clone_from(¤t);
754 }
755
756 previous[right_chars.len()]
757}
758
759fn canonical_cycle_key(cycle: &[String]) -> String {
760 let nodes = &cycle[..cycle.len().saturating_sub(1)];
761 if nodes.is_empty() {
762 return String::new();
763 }
764
765 let mut best = None;
766 for start in 0..nodes.len() {
767 let rotation = (0..nodes.len())
768 .map(|offset| nodes[(start + offset) % nodes.len()].as_str())
769 .collect::<Vec<_>>()
770 .join("\0");
771 if best
772 .as_ref()
773 .map_or(true, |candidate| rotation < *candidate)
774 {
775 best = Some(rotation);
776 }
777 }
778 best.unwrap_or_default()
779}
780
781fn sort_diagnostics(grammar: &Grammar, diagnostics: &mut [GrammarDiagnostic]) {
782 let indices = grammar
783 .rules()
784 .iter()
785 .enumerate()
786 .rev()
787 .map(|(index, rule)| (rule.name.clone(), index))
788 .collect::<BTreeMap<_, _>>();
789
790 diagnostics.sort_by(|left, right| {
791 let left_span = left
792 .location
793 .span
794 .as_ref()
795 .map_or(usize::MAX, |span| span.start);
796 let right_span = right
797 .location
798 .span
799 .as_ref()
800 .map_or(usize::MAX, |span| span.start);
801 let left_index = indices
802 .get(&left.location.rule)
803 .copied()
804 .unwrap_or(usize::MAX);
805 let right_index = indices
806 .get(&right.location.rule)
807 .copied()
808 .unwrap_or(usize::MAX);
809
810 left_span
811 .cmp(&right_span)
812 .then_with(|| left_index.cmp(&right_index))
813 .then_with(|| left.location.rule.cmp(&right.location.rule))
814 .then_with(|| severity_rank(left.severity).cmp(&severity_rank(right.severity)))
815 .then_with(|| kind_rank(&left.kind).cmp(&kind_rank(&right.kind)))
816 .then_with(|| left.message.cmp(&right.message))
817 });
818}
819
820const fn severity_rank(severity: Severity) -> usize {
821 match severity {
822 Severity::Error => 0,
823 Severity::Warning => 1,
824 }
825}
826
827const fn kind_rank(kind: &DiagnosticKind) -> usize {
828 match kind {
829 DiagnosticKind::DuplicateRule { .. } => 0,
830 DiagnosticKind::UndefinedNonTerminal { .. } => 1,
831 DiagnosticKind::LeftRecursion { .. } => 2,
832 DiagnosticKind::UnreachableRule { .. } => 3,
833 DiagnosticKind::NullableRepetition { .. } => 4,
834 DiagnosticKind::UnusedCapture { .. } => 5,
835 }
836}
837
838fn rule_location(rule: impl Into<String>) -> RuleSpan {
839 RuleSpan {
840 rule: rule.into(),
841 span: None,
842 }
843}