plotnik_lib/parser/
grammar.rs

1//! Grammar productions for the query language.
2//!
3//! This module implements all `parse_*` methods as an extension of `Parser`.
4//! The grammar follows tree-sitter query syntax with extensions for named subqueries.
5
6use rowan::{Checkpoint, TextRange};
7
8use super::core::Parser;
9
10use super::cst::token_sets::{
11    ALT_RECOVERY, EXPR_FIRST, QUANTIFIERS, SEPARATORS, SEQ_RECOVERY, TREE_RECOVERY,
12};
13use super::cst::{SyntaxKind, TokenSet};
14use super::lexer::token_text;
15use crate::diagnostics::DiagnosticKind;
16
17impl Parser<'_> {
18    pub fn parse_root(&mut self) {
19        self.start_node(SyntaxKind::Root);
20
21        let mut unnamed_def_spans: Vec<TextRange> = Vec::new();
22
23        while !self.has_fatal_error() && (self.peek() != SyntaxKind::Error || !self.eof()) {
24            // LL(2): Id followed by Equals → named definition (if PascalCase)
25            if self.peek() == SyntaxKind::Id && self.peek_nth(1) == SyntaxKind::Equals {
26                self.parse_def();
27            } else {
28                let start = self.current_span().start();
29                self.start_node(SyntaxKind::Def);
30                let success = self.parse_expr_or_error();
31                if !success {
32                    self.synchronize_to_def_start();
33                }
34                self.finish_node();
35                if success {
36                    let end = self.last_non_trivia_end().unwrap_or(start);
37                    unnamed_def_spans.push(TextRange::new(start, end));
38                }
39            }
40        }
41
42        if unnamed_def_spans.len() > 1 {
43            for span in &unnamed_def_spans[..unnamed_def_spans.len() - 1] {
44                let def_text = &self.source[usize::from(span.start())..usize::from(span.end())];
45                self.diagnostics
46                    .report(DiagnosticKind::UnnamedDefNotLast, *span)
47                    .message(format!("give it a name like `Name = {}`", def_text.trim()))
48                    .emit();
49            }
50        }
51
52        self.eat_trivia();
53        self.finish_node();
54    }
55
56    /// Named expression definition: `Name = expr`
57    fn parse_def(&mut self) {
58        self.start_node(SyntaxKind::Def);
59
60        let span = self.current_span();
61        let name = token_text(self.source, &self.tokens[self.pos]);
62        self.bump();
63        self.validate_def_name(name, span);
64
65        self.peek();
66        let ate_equals = self.eat(SyntaxKind::Equals);
67        assert!(
68            ate_equals,
69            "parse_def: expected '=' but found {:?} (caller should verify Equals is present)",
70            self.current()
71        );
72
73        if EXPR_FIRST.contains(self.peek()) {
74            self.parse_expr();
75        } else {
76            self.error_msg(
77                DiagnosticKind::ExpectedExpression,
78                "after `=` in definition",
79            );
80        }
81
82        self.finish_node();
83    }
84
85    /// Parse an expression, or emit an error if current token can't start one.
86    /// Returns `true` if a valid expression was parsed, `false` on error.
87    fn parse_expr_or_error(&mut self) -> bool {
88        let kind = self.peek();
89        if EXPR_FIRST.contains(kind) {
90            self.parse_expr();
91            true
92        } else if kind == SyntaxKind::At {
93            self.error_and_bump(DiagnosticKind::CaptureWithoutTarget);
94            false
95        } else if kind == SyntaxKind::Predicate {
96            self.error_and_bump(DiagnosticKind::UnsupportedPredicate);
97            false
98        } else {
99            self.error_and_bump_msg(
100                DiagnosticKind::UnexpectedToken,
101                "try `(node)`, `[a b]`, `{a b}`, `\"literal\"`, or `_`",
102            );
103            false
104        }
105    }
106
107    /// Core recursive descent. Dispatches based on lookahead, then checks for quantifier/capture suffix.
108    fn parse_expr(&mut self) {
109        self.parse_expr_inner(true)
110    }
111
112    /// Parse expression without applying quantifier/capture suffix.
113    /// Used for field values so that `field: (x)*` parses as `(field: (x))*`.
114    fn parse_expr_no_suffix(&mut self) {
115        self.parse_expr_inner(false)
116    }
117
118    fn parse_expr_inner(&mut self, with_suffix: bool) {
119        if !self.enter_recursion() {
120            self.start_node(SyntaxKind::Error);
121            while !self.should_stop() {
122                self.bump();
123            }
124            self.finish_node();
125            return;
126        }
127
128        let checkpoint = self.checkpoint();
129
130        match self.peek() {
131            SyntaxKind::ParenOpen => self.parse_tree(),
132            SyntaxKind::BracketOpen => self.parse_alt(),
133            SyntaxKind::BraceOpen => self.parse_seq(),
134            SyntaxKind::Underscore => self.parse_wildcard(),
135            SyntaxKind::SingleQuote | SyntaxKind::DoubleQuote => self.parse_str(),
136            SyntaxKind::Dot => self.parse_anchor(),
137            SyntaxKind::Negation => self.parse_negated_field(),
138            SyntaxKind::Id => self.parse_tree_or_field(),
139            SyntaxKind::KwError | SyntaxKind::KwMissing => {
140                self.error_and_bump(DiagnosticKind::ErrorMissingOutsideParens);
141            }
142            _ => {
143                self.error_and_bump_msg(DiagnosticKind::UnexpectedToken, "not a valid expression");
144            }
145        }
146
147        if with_suffix {
148            self.try_parse_quantifier(checkpoint);
149            self.try_parse_capture(checkpoint);
150        }
151
152        self.exit_recursion();
153    }
154
155    /// `(type ...)` | `(_ ...)` | `(ERROR)` | `(MISSING ...)` | `(RefName)` | `(expr/subtype)`
156    /// PascalCase without children → Ref; with children → error but parses as Tree.
157    fn parse_tree(&mut self) {
158        let checkpoint = self.checkpoint();
159        self.push_delimiter(SyntaxKind::ParenOpen);
160        self.bump(); // consume '('
161
162        let mut is_ref = false;
163        let mut ref_name: Option<String> = None;
164
165        match self.peek() {
166            SyntaxKind::ParenClose => {
167                self.start_node_at(checkpoint, SyntaxKind::Tree);
168                self.error(DiagnosticKind::EmptyTree);
169                self.pop_delimiter();
170                self.bump(); // consume ')'
171                self.finish_node();
172                return;
173            }
174            SyntaxKind::Underscore => {
175                self.start_node_at(checkpoint, SyntaxKind::Tree);
176                self.bump();
177            }
178            SyntaxKind::Id => {
179                let name = token_text(self.source, &self.tokens[self.pos]).to_string();
180                let is_pascal_case = name.chars().next().is_some_and(|c| c.is_ascii_uppercase());
181                self.bump();
182
183                if is_pascal_case {
184                    is_ref = true;
185                    ref_name = Some(name);
186                } else {
187                    self.start_node_at(checkpoint, SyntaxKind::Tree);
188                }
189
190                if self.peek() == SyntaxKind::Slash {
191                    if is_ref {
192                        self.start_node_at(checkpoint, SyntaxKind::Tree);
193                        self.error(DiagnosticKind::InvalidSupertypeSyntax);
194                        is_ref = false;
195                    }
196                    self.bump();
197                    match self.peek() {
198                        SyntaxKind::Id => {
199                            self.bump();
200                        }
201                        SyntaxKind::SingleQuote | SyntaxKind::DoubleQuote => {
202                            self.bump_string_tokens();
203                        }
204                        _ => {
205                            self.error_msg(
206                                DiagnosticKind::ExpectedSubtype,
207                                "e.g., `expression/binary_expression`",
208                            );
209                        }
210                    }
211                }
212            }
213            SyntaxKind::KwError => {
214                self.start_node_at(checkpoint, SyntaxKind::Tree);
215                self.bump();
216                if self.peek() != SyntaxKind::ParenClose {
217                    self.error(DiagnosticKind::ErrorTakesNoArguments);
218                    self.parse_children(SyntaxKind::ParenClose, TREE_RECOVERY);
219                }
220                self.pop_delimiter();
221                self.expect(SyntaxKind::ParenClose, "closing ')' for (ERROR)");
222                self.finish_node();
223                return;
224            }
225            SyntaxKind::KwMissing => {
226                self.start_node_at(checkpoint, SyntaxKind::Tree);
227                self.bump();
228                match self.peek() {
229                    SyntaxKind::Id => {
230                        self.bump();
231                    }
232                    SyntaxKind::SingleQuote | SyntaxKind::DoubleQuote => {
233                        self.bump_string_tokens();
234                    }
235                    SyntaxKind::ParenClose => {}
236                    _ => {
237                        self.parse_children(SyntaxKind::ParenClose, TREE_RECOVERY);
238                    }
239                }
240                self.pop_delimiter();
241                self.expect(SyntaxKind::ParenClose, "closing ')' for (MISSING)");
242                self.finish_node();
243                return;
244            }
245            _ => {
246                self.start_node_at(checkpoint, SyntaxKind::Tree);
247            }
248        }
249
250        let has_children = self.peek() != SyntaxKind::ParenClose;
251
252        if is_ref && has_children {
253            self.start_node_at(checkpoint, SyntaxKind::Tree);
254            let children_start = self.current_span().start();
255            self.parse_children(SyntaxKind::ParenClose, TREE_RECOVERY);
256            let children_end = self.last_non_trivia_end().unwrap_or(children_start);
257            let children_span = TextRange::new(children_start, children_end);
258
259            if let Some(name) = &ref_name {
260                self.diagnostics
261                    .report(DiagnosticKind::RefCannotHaveChildren, children_span)
262                    .message(name)
263                    .emit();
264            }
265        } else if is_ref {
266            self.start_node_at(checkpoint, SyntaxKind::Ref);
267        } else {
268            self.parse_children(SyntaxKind::ParenClose, TREE_RECOVERY);
269        }
270
271        self.pop_delimiter();
272        self.expect(
273            SyntaxKind::ParenClose,
274            if is_ref && !has_children {
275                "closing ')' for reference"
276            } else {
277                "closing ')' for tree"
278            },
279        );
280        self.finish_node();
281    }
282
283    /// Parse children until `until` token or recovery set hit.
284    fn parse_children(&mut self, until: SyntaxKind, recovery: TokenSet) {
285        loop {
286            if self.eof() {
287                let (construct, delim, kind) = match until {
288                    SyntaxKind::ParenClose => ("tree", "`)`", DiagnosticKind::UnclosedTree),
289                    SyntaxKind::BraceClose => ("sequence", "`}`", DiagnosticKind::UnclosedSequence),
290                    _ => panic!(
291                        "parse_children: unexpected delimiter {:?} (only ParenClose/BraceClose supported)",
292                        until
293                    ),
294                };
295                let msg = format!("expected {delim}");
296                let open = self.delimiter_stack.last().unwrap_or_else(|| {
297                    panic!(
298                        "parse_children: unclosed {construct} at EOF but delimiter_stack is empty \
299                         (caller must push delimiter before calling)"
300                    )
301                });
302                self.error_unclosed_delimiter(
303                    kind,
304                    msg,
305                    format!("{construct} started here"),
306                    open.span,
307                );
308                break;
309            }
310            if self.has_fatal_error() {
311                break;
312            }
313            let kind = self.peek();
314            if kind == until {
315                break;
316            }
317            if SEPARATORS.contains(kind) {
318                self.error_skip_separator();
319                continue;
320            }
321            if EXPR_FIRST.contains(kind) {
322                self.parse_expr();
323                continue;
324            }
325            if kind == SyntaxKind::Predicate {
326                self.error_and_bump(DiagnosticKind::UnsupportedPredicate);
327                continue;
328            }
329            if recovery.contains(kind) {
330                break;
331            }
332            self.error_and_bump_msg(
333                DiagnosticKind::UnexpectedToken,
334                "not valid inside a node — try `(child)` or close with `)`",
335            );
336        }
337    }
338
339    /// Alternation/choice: `[expr1 expr2 ...]` or `[Label: expr ...]`
340    fn parse_alt(&mut self) {
341        self.start_node(SyntaxKind::Alt);
342        self.push_delimiter(SyntaxKind::BracketOpen);
343        self.expect(SyntaxKind::BracketOpen, "opening '[' for alternation");
344
345        self.parse_alt_children();
346
347        self.pop_delimiter();
348        self.expect(SyntaxKind::BracketClose, "closing ']' for alternation");
349        self.finish_node();
350    }
351
352    /// Parse alternation children, handling both tagged `Label: expr` and unlabeled expressions.
353    fn parse_alt_children(&mut self) {
354        loop {
355            if self.eof() {
356                let msg = "expected `]`";
357                let open = self.delimiter_stack.last().unwrap_or_else(|| {
358                    panic!(
359                        "parse_alt_children: unclosed alternation at EOF but delimiter_stack is empty \
360                         (caller must push delimiter before calling)"
361                    )
362                });
363                self.error_unclosed_delimiter(
364                    DiagnosticKind::UnclosedAlternation,
365                    msg,
366                    "alternation started here",
367                    open.span,
368                );
369                break;
370            }
371            if self.has_fatal_error() {
372                break;
373            }
374            let kind = self.peek();
375            if kind == SyntaxKind::BracketClose {
376                break;
377            }
378            if SEPARATORS.contains(kind) {
379                self.error_skip_separator();
380                continue;
381            }
382
383            // LL(2): Id followed by Colon → branch label or field (check casing)
384            if kind == SyntaxKind::Id && self.peek_nth(1) == SyntaxKind::Colon {
385                let text = token_text(self.source, &self.tokens[self.pos]);
386                let first_char = text.chars().next().unwrap_or('a');
387                if first_char.is_ascii_uppercase() {
388                    self.parse_branch();
389                } else {
390                    self.parse_branch_lowercase_label();
391                }
392                continue;
393            }
394            if EXPR_FIRST.contains(kind) {
395                self.start_node(SyntaxKind::Branch);
396                self.parse_expr();
397                self.finish_node();
398                continue;
399            }
400            if ALT_RECOVERY.contains(kind) {
401                break;
402            }
403            self.error_and_bump_msg(
404                DiagnosticKind::UnexpectedToken,
405                "not valid inside alternation — try `(node)` or close with `]`",
406            );
407        }
408    }
409
410    /// Tagged alternation branch: `Label: expr`
411    fn parse_branch(&mut self) {
412        self.start_node(SyntaxKind::Branch);
413
414        let span = self.current_span();
415        let text = token_text(self.source, &self.tokens[self.pos]);
416        self.bump();
417        self.validate_branch_label(text, span);
418
419        self.peek();
420        self.expect(SyntaxKind::Colon, "':' after branch label");
421
422        if EXPR_FIRST.contains(self.peek()) {
423            self.parse_expr();
424        } else {
425            self.error_msg(DiagnosticKind::ExpectedExpression, "after `Label:`");
426        }
427
428        self.finish_node();
429    }
430
431    /// Parse a branch with lowercase label - parse as Branch but emit error.
432    fn parse_branch_lowercase_label(&mut self) {
433        self.start_node(SyntaxKind::Branch);
434
435        let span = self.current_span();
436        let label_text = token_text(self.source, &self.tokens[self.pos]);
437        let capitalized = capitalize_first(label_text);
438
439        self.error_with_fix(
440            DiagnosticKind::LowercaseBranchLabel,
441            span,
442            "branch labels map to enum variants",
443            format!("use `{}`", capitalized),
444            capitalized,
445        );
446
447        self.bump();
448        self.peek();
449        self.expect(SyntaxKind::Colon, "':' after branch label");
450
451        if EXPR_FIRST.contains(self.peek()) {
452            self.parse_expr();
453        } else {
454            self.error_msg(DiagnosticKind::ExpectedExpression, "after `label:`");
455        }
456
457        self.finish_node();
458    }
459
460    /// Sibling sequence: `{expr1 expr2 ...}`
461    fn parse_seq(&mut self) {
462        self.start_node(SyntaxKind::Seq);
463        self.push_delimiter(SyntaxKind::BraceOpen);
464        self.expect(SyntaxKind::BraceOpen, "opening '{' for sequence");
465
466        self.parse_children(SyntaxKind::BraceClose, SEQ_RECOVERY);
467
468        self.pop_delimiter();
469        self.expect(SyntaxKind::BraceClose, "closing '}' for sequence");
470        self.finish_node();
471    }
472
473    fn parse_wildcard(&mut self) {
474        self.start_node(SyntaxKind::Wildcard);
475        self.expect(SyntaxKind::Underscore, "'_' wildcard");
476        self.finish_node();
477    }
478
479    /// `"if"` | `'+'`
480    fn parse_str(&mut self) {
481        self.start_node(SyntaxKind::Str);
482        self.bump_string_tokens();
483        self.finish_node();
484    }
485
486    /// Consume string tokens (quote + optional content + quote) without creating a node.
487    /// Used for contexts where string appears as a raw value (supertype, MISSING arg).
488    fn bump_string_tokens(&mut self) {
489        let open_quote = self.peek();
490        self.bump(); // opening quote
491
492        if self.peek() == SyntaxKind::StrVal {
493            self.bump(); // content
494        }
495
496        let closing = self.peek();
497        assert_eq!(
498            closing, open_quote,
499            "bump_string_tokens: expected closing {:?} but found {:?} \
500             (lexer should only produce quote tokens from complete strings)",
501            open_quote, closing
502        );
503        self.bump();
504    }
505
506    /// `@name` | `@name :: Type`
507    fn parse_capture_suffix(&mut self) {
508        self.bump(); // consume At
509
510        if self.peek() != SyntaxKind::Id {
511            self.error(DiagnosticKind::ExpectedCaptureName);
512            return;
513        }
514
515        let span = self.current_span();
516        let name = token_text(self.source, &self.tokens[self.pos]);
517        self.bump(); // consume Id
518
519        self.validate_capture_name(name, span);
520
521        if self.peek() == SyntaxKind::Colon {
522            self.parse_type_annotation_single_colon();
523            return;
524        }
525        if self.peek() == SyntaxKind::DoubleColon {
526            self.parse_type_annotation();
527        }
528    }
529
530    /// Type annotation: `::Type` (PascalCase) or `::string` (primitive)
531    fn parse_type_annotation(&mut self) {
532        self.start_node(SyntaxKind::Type);
533        self.expect(SyntaxKind::DoubleColon, "'::' for type annotation");
534
535        if self.peek() == SyntaxKind::Id {
536            let span = self.current_span();
537            let text = token_text(self.source, &self.tokens[self.pos]);
538            self.bump();
539            self.validate_type_name(text, span);
540        } else {
541            self.error_msg(
542                DiagnosticKind::ExpectedTypeName,
543                "e.g., `::MyType` or `::string`",
544            );
545        }
546
547        self.finish_node();
548    }
549
550    /// Handle single colon type annotation (common mistake: `@x : Type` instead of `@x :: Type`)
551    fn parse_type_annotation_single_colon(&mut self) {
552        if self.peek_nth(1) != SyntaxKind::Id {
553            return;
554        }
555
556        self.start_node(SyntaxKind::Type);
557
558        let span = self.current_span();
559        self.error_with_fix(
560            DiagnosticKind::InvalidTypeAnnotationSyntax,
561            span,
562            "single `:` looks like a field",
563            "use `::`",
564            "::",
565        );
566
567        self.bump(); // colon
568
569        // peek() skips whitespace, so this handles `@x : Type` with space
570        if self.peek() == SyntaxKind::Id {
571            self.bump();
572        }
573
574        self.finish_node();
575    }
576
577    /// `.` anchor
578    fn parse_anchor(&mut self) {
579        self.start_node(SyntaxKind::Anchor);
580        self.expect(SyntaxKind::Dot, "'.' anchor");
581        self.finish_node();
582    }
583
584    /// Negated field assertion: `!field` (field must be absent)
585    fn parse_negated_field(&mut self) {
586        self.start_node(SyntaxKind::NegatedField);
587        self.expect(SyntaxKind::Negation, "'!' for negated field");
588
589        if self.peek() != SyntaxKind::Id {
590            self.error_msg(DiagnosticKind::ExpectedFieldName, "e.g., `!value`");
591            self.finish_node();
592            return;
593        }
594
595        let span = self.current_span();
596        let text = token_text(self.source, &self.tokens[self.pos]);
597        self.bump();
598        self.validate_field_name(text, span);
599        self.finish_node();
600    }
601
602    /// Disambiguate `field: expr` from bare identifier via LL(2) lookahead.
603    /// Also handles `field = expr` typo (should be `field: expr`).
604    fn parse_tree_or_field(&mut self) {
605        if self.peek_nth(1) == SyntaxKind::Colon {
606            self.parse_field();
607        } else if self.peek_nth(1) == SyntaxKind::Equals {
608            self.parse_field_equals_typo();
609        } else {
610            // Bare identifiers are not valid expressions; trees require parentheses
611            self.error_and_bump_msg(
612                DiagnosticKind::BareIdentifier,
613                "wrap in parentheses: `(identifier)`",
614            );
615        }
616    }
617
618    /// Field constraint: `field_name: expr`
619    fn parse_field(&mut self) {
620        self.start_node(SyntaxKind::Field);
621
622        let kind = self.peek();
623        assert_eq!(
624            kind,
625            SyntaxKind::Id,
626            "parse_field: expected Id but found {:?} (caller should verify Id is present)",
627            kind
628        );
629        let span = self.current_span();
630        let text = token_text(self.source, &self.tokens[self.pos]);
631        self.bump();
632        self.validate_field_name(text, span);
633
634        self.expect(
635            SyntaxKind::Colon,
636            "':' to separate field name from its value",
637        );
638
639        if EXPR_FIRST.contains(self.peek()) {
640            self.parse_expr_no_suffix();
641        } else {
642            self.error_msg(DiagnosticKind::ExpectedExpression, "after `field:`");
643        }
644
645        self.finish_node();
646    }
647
648    /// Handle `field = expr` typo - parse as Field but emit error.
649    fn parse_field_equals_typo(&mut self) {
650        self.start_node(SyntaxKind::Field);
651
652        self.bump();
653        self.peek();
654        let span = self.current_span();
655        self.error_with_fix(
656            DiagnosticKind::InvalidFieldEquals,
657            span,
658            "this isn't a definition",
659            "use `:`",
660            ":",
661        );
662        self.bump();
663
664        if EXPR_FIRST.contains(self.peek()) {
665            self.parse_expr();
666        } else {
667            self.error_msg(DiagnosticKind::ExpectedExpression, "after `field =`");
668        }
669
670        self.finish_node();
671    }
672
673    /// Skip a separator token (comma or pipe) and emit helpful error.
674    fn error_skip_separator(&mut self) {
675        let kind = self.current();
676        let span = self.current_span();
677        // Invariant: only called when SEPARATORS.contains(kind), which only has Comma and Pipe
678        let char_name = match kind {
679            SyntaxKind::Comma => ",",
680            SyntaxKind::Pipe => "|",
681            _ => panic!(
682                "error_skip_separator: unexpected token {:?} (only Comma/Pipe expected)",
683                kind
684            ),
685        };
686        self.error_with_fix(
687            DiagnosticKind::InvalidSeparator,
688            span,
689            format!("plotnik uses whitespace, not `{}`", char_name),
690            "remove",
691            "",
692        );
693        self.skip_token();
694    }
695
696    /// If current token is quantifier, wrap preceding expression using checkpoint.
697    fn try_parse_quantifier(&mut self, checkpoint: Checkpoint) {
698        if self.at_set(QUANTIFIERS) {
699            self.start_node_at(checkpoint, SyntaxKind::Quantifier);
700            self.bump();
701            self.finish_node();
702        }
703    }
704
705    /// If current token is a capture (`@name`), wrap preceding expression with Capture using checkpoint.
706    fn try_parse_capture(&mut self, checkpoint: Checkpoint) {
707        if self.peek() == SyntaxKind::At {
708            self.start_node_at(checkpoint, SyntaxKind::Capture);
709            self.drain_trivia();
710            self.parse_capture_suffix();
711            self.finish_node();
712        }
713    }
714
715    /// Validate capture name follows plotnik convention (snake_case).
716    fn validate_capture_name(&mut self, name: &str, span: TextRange) {
717        if name.contains('.') {
718            let suggested = name.replace(['.', '-'], "_");
719            let suggested = to_snake_case(&suggested);
720            self.error_with_fix(
721                DiagnosticKind::CaptureNameHasDots,
722                span,
723                "captures become struct fields",
724                format!("use `@{}`", suggested),
725                suggested,
726            );
727            return;
728        }
729
730        if name.contains('-') {
731            let suggested = name.replace('-', "_");
732            let suggested = to_snake_case(&suggested);
733            self.error_with_fix(
734                DiagnosticKind::CaptureNameHasHyphens,
735                span,
736                "captures become struct fields",
737                format!("use `@{}`", suggested),
738                suggested,
739            );
740            return;
741        }
742
743        if name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
744            let suggested = to_snake_case(name);
745            self.error_with_fix(
746                DiagnosticKind::CaptureNameUppercase,
747                span,
748                "captures become struct fields",
749                format!("use `@{}`", suggested),
750                suggested,
751            );
752        }
753    }
754
755    /// Validate definition name follows PascalCase convention.
756    fn validate_def_name(&mut self, name: &str, span: TextRange) {
757        if !name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
758            let suggested = to_pascal_case(name);
759            self.error_with_fix(
760                DiagnosticKind::DefNameLowercase,
761                span,
762                "definitions map to types",
763                format!("use `{}`", suggested),
764                suggested,
765            );
766            return;
767        }
768
769        if name.contains('_') || name.contains('-') || name.contains('.') {
770            let suggested = to_pascal_case(name);
771            self.error_with_fix(
772                DiagnosticKind::DefNameHasSeparators,
773                span,
774                "definitions map to types",
775                format!("use `{}`", suggested),
776                suggested,
777            );
778        }
779    }
780
781    /// Validate branch label follows PascalCase convention.
782    fn validate_branch_label(&mut self, name: &str, span: TextRange) {
783        if name.contains('_') || name.contains('-') || name.contains('.') {
784            let suggested = to_pascal_case(name);
785            self.error_with_fix(
786                DiagnosticKind::BranchLabelHasSeparators,
787                span,
788                "branch labels map to enum variants",
789                format!("use `{}:`", suggested),
790                format!("{}:", suggested),
791            );
792        }
793    }
794
795    /// Validate field name follows snake_case convention.
796    fn validate_field_name(&mut self, name: &str, span: TextRange) {
797        if name.contains('.') {
798            let suggested = name.replace(['.', '-'], "_");
799            let suggested = to_snake_case(&suggested);
800            self.error_with_fix(
801                DiagnosticKind::FieldNameHasDots,
802                span,
803                "field names become struct fields",
804                format!("use `{}:`", suggested),
805                format!("{}:", suggested),
806            );
807            return;
808        }
809
810        if name.contains('-') {
811            let suggested = name.replace('-', "_");
812            let suggested = to_snake_case(&suggested);
813            self.error_with_fix(
814                DiagnosticKind::FieldNameHasHyphens,
815                span,
816                "field names become struct fields",
817                format!("use `{}:`", suggested),
818                format!("{}:", suggested),
819            );
820            return;
821        }
822
823        if name.chars().next().is_some_and(|c| c.is_ascii_uppercase()) {
824            let suggested = to_snake_case(name);
825            self.error_with_fix(
826                DiagnosticKind::FieldNameUppercase,
827                span,
828                "field names become struct fields",
829                format!("use `{}:`", suggested),
830                format!("{}:", suggested),
831            );
832        }
833    }
834
835    /// Validate type annotation name (PascalCase for user types, snake_case for primitives allowed).
836    fn validate_type_name(&mut self, name: &str, span: TextRange) {
837        if name.contains('.') || name.contains('-') {
838            let suggested = to_pascal_case(name);
839            self.error_with_fix(
840                DiagnosticKind::TypeNameInvalidChars,
841                span,
842                "type annotations map to types",
843                format!("use `::{}`", suggested),
844                format!("::{}", suggested),
845            );
846        }
847    }
848}
849
850fn to_snake_case(s: &str) -> String {
851    let mut result = String::new();
852    for (i, c) in s.chars().enumerate() {
853        if c.is_ascii_uppercase() {
854            if i > 0 && !result.ends_with('_') {
855                result.push('_');
856            }
857            result.push(c.to_ascii_lowercase());
858        } else {
859            result.push(c);
860        }
861    }
862    result
863}
864
865fn to_pascal_case(s: &str) -> String {
866    let mut result = String::new();
867    let mut capitalize_next = true;
868    for c in s.chars() {
869        if c == '_' || c == '-' || c == '.' {
870            capitalize_next = true;
871        } else if capitalize_next {
872            result.push(c.to_ascii_uppercase());
873            capitalize_next = false;
874        } else {
875            result.push(c.to_ascii_lowercase());
876        }
877    }
878    result
879}
880
881fn capitalize_first(s: &str) -> String {
882    assert!(!s.is_empty(), "capitalize_first: called with empty string");
883    let mut chars = s.chars();
884    let c = chars.next().unwrap();
885    c.to_uppercase().chain(chars).collect()
886}