Skip to main content

only_syntax/
parse.rs

1use only_diagnostic::{Diagnostic, DiagnosticCode, DiagnosticPhase, DiagnosticSeverity};
2use rowan::SyntaxNodeChildren;
3use text_size::{TextRange, TextSize};
4use winnow::Parser;
5use winnow::combinator::alt;
6use winnow::error::{ContextError, ErrMode, ModalResult};
7use winnow::token::any;
8
9use crate::ast_view::DocumentNode;
10use crate::builder::ParseTreeBuilder;
11use crate::cst::SyntaxNode;
12use crate::cursor::TokenCursor;
13use crate::recover::{
14    advance, consume_line, starts_indented_namespace_boundary, starts_indented_namespace_member,
15    starts_top_level_item,
16};
17use crate::trivia::{is_trivia, line_contains_kind, line_has_non_trivia};
18use crate::{LexToken, SyntaxKind, lex};
19
20#[derive(Debug, Clone)]
21pub struct ParseResult {
22    pub root: SyntaxNode,
23    diagnostics: Vec<Diagnostic>,
24}
25
26impl ParseResult {
27    /// Returns the typed document CST root.
28    ///
29    /// Args:
30    /// None.
31    ///
32    /// Returns:
33    /// Typed document wrapper for the parse root.
34    pub fn document(&self) -> DocumentNode {
35        DocumentNode::cast(self.root.clone()).expect("parse root must always be a document node")
36    }
37}
38
39/// Extension helpers for parse results used by hosts and tests.
40pub trait ParseResultExt {
41    /// Returns root CST children for top-level inspection.
42    fn root_children(&self) -> SyntaxNodeChildren<crate::cst::OnlyLanguage>;
43
44    /// Returns collected parse diagnostics.
45    fn diagnostics(&self) -> &[Diagnostic];
46}
47
48impl ParseResultExt for ParseResult {
49    fn root_children(&self) -> SyntaxNodeChildren<crate::cst::OnlyLanguage> {
50        self.root.children()
51    }
52
53    fn diagnostics(&self) -> &[Diagnostic] {
54        &self.diagnostics
55    }
56}
57
58/// Parses Onlyfile text into a shallow CST with line-level recovery.
59///
60/// Args:
61/// source: Raw Onlyfile source text.
62///
63/// Returns:
64/// Parse result containing CST root and collected diagnostics.
65pub fn parse(source: &str) -> ParseResult {
66    let tokens = lex(source);
67    parse_tokens(&tokens)
68}
69
70pub(crate) fn parse_tokens(tokens: &[LexToken]) -> ParseResult {
71    let mut builder = ParseTreeBuilder::new();
72    let mut diagnostics = Vec::new();
73    let kinds = tokens.iter().map(|token| token.kind).collect::<Vec<_>>();
74    let mut cursor = TokenCursor::new(tokens, &kinds);
75    let mut in_braced_namespace = false;
76
77    loop {
78        let trivia = cursor.skip_trivia();
79        builder.push_tokens(trivia);
80
81        let Some(token) = cursor.current() else {
82            break;
83        };
84        if token.kind == SyntaxKind::Eof {
85            break;
86        }
87
88        let mut input = cursor.remaining();
89        let (item, consumed) =
90            (|input: &mut &[SyntaxKind]| parse_top_level_item(input, in_braced_namespace))
91                .with_taken()
92                .parse_next(&mut input)
93                .expect("top-level parser should always consume a non-EOF item");
94        let token_slice = cursor.consume(consumed.len());
95
96        match item {
97            ParsedTopLevelItem::Directive { malformed } => {
98                if malformed {
99                    diagnostics.push(parse_error(
100                        "parse.malformed-directive",
101                        "invalid directive",
102                        token.range,
103                    ));
104                    builder.push_node(SyntaxKind::Error, token_slice);
105                    continue;
106                }
107                builder.push_node(SyntaxKind::Directive, token_slice);
108            }
109            ParsedTopLevelItem::MetadataComment => {
110                builder.push_node(SyntaxKind::MetadataComment, token_slice);
111            }
112            ParsedTopLevelItem::Namespace {
113                malformed,
114                is_close,
115                has_open_brace,
116            } => {
117                if malformed {
118                    diagnostics.push(parse_error(
119                        "parse.malformed-namespace-header",
120                        "invalid namespace",
121                        token.range,
122                    ));
123                    builder.push_node(SyntaxKind::Error, token_slice);
124                    continue;
125                }
126                builder.push_node(SyntaxKind::NamespaceBlock, token_slice);
127                in_braced_namespace = has_open_brace && !is_close;
128            }
129            ParsedTopLevelItem::Task {
130                saw_colon,
131                malformed,
132            } => {
133                if !saw_colon || malformed {
134                    diagnostics.push(parse_error(
135                        "parse.malformed-task-header",
136                        "invalid task header",
137                        token.range,
138                    ));
139                    builder.push_node(SyntaxKind::Error, token_slice);
140                    continue;
141                }
142                builder.push_task(token_slice);
143            }
144            ParsedTopLevelItem::Unexpected => {
145                diagnostics.push(parse_error(
146                    "parse.unexpected-token",
147                    "unexpected text",
148                    token.range,
149                ));
150                builder.push_node(SyntaxKind::Error, token_slice);
151            }
152        }
153    }
154
155    ParseResult {
156        root: builder.finish(),
157        diagnostics,
158    }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162enum ParsedTopLevelItem {
163    Directive {
164        malformed: bool,
165    },
166    MetadataComment,
167    Namespace {
168        malformed: bool,
169        is_close: bool,
170        has_open_brace: bool,
171    },
172    Task {
173        saw_colon: bool,
174        malformed: bool,
175    },
176    Unexpected,
177}
178
179fn parse_top_level_item(
180    input: &mut &[SyntaxKind],
181    in_braced_namespace: bool,
182) -> ModalResult<ParsedTopLevelItem> {
183    alt((
184        parse_directive_item,
185        parse_metadata_item,
186        parse_namespace_item,
187        |input: &mut &[SyntaxKind]| parse_task_item(input, in_braced_namespace),
188        parse_unexpected_item,
189    ))
190    .parse_next(input)
191}
192
193fn parse_directive_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
194    token_kind(input, SyntaxKind::Bang)?;
195    let malformed = !line_has_non_trivia(input) || line_contains_kind(input, SyntaxKind::Comment);
196    consume_line(input);
197    Ok(ParsedTopLevelItem::Directive { malformed })
198}
199
200fn parse_metadata_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
201    token_kind(input, SyntaxKind::LBracket)?;
202    token_kind(input, SyntaxKind::Ident)?;
203    token_kind(input, SyntaxKind::RBracket)?;
204
205    let mut rest = *input;
206    while matches!(rest.first(), Some(SyntaxKind::Whitespace)) {
207        advance(&mut rest);
208    }
209    let starts_namespace_brace =
210        rest.first() == Some(&SyntaxKind::LBrace) && rest.get(1) != Some(&SyntaxKind::LBrace);
211    let has_text = !matches!(rest.first(), Some(SyntaxKind::Newline | SyntaxKind::Eof))
212        && !starts_namespace_brace
213        && rest.first() != Some(&SyntaxKind::Comment);
214    if !has_text {
215        return Err(ErrMode::Backtrack(ContextError::new()));
216    }
217
218    consume_line(input);
219    Ok(ParsedTopLevelItem::MetadataComment)
220}
221
222fn parse_namespace_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
223    if input.first() == Some(&SyntaxKind::RBrace) {
224        advance(input);
225        let malformed =
226            line_has_non_trivia(input) || line_contains_kind(input, SyntaxKind::Comment);
227        consume_line(input);
228        return Ok(ParsedTopLevelItem::Namespace {
229            malformed,
230            is_close: true,
231            has_open_brace: false,
232        });
233    }
234
235    if input.first() == Some(&SyntaxKind::GroupKw)
236        && input.get(1) == Some(&SyntaxKind::Whitespace)
237        && input.get(2) == Some(&SyntaxKind::Ident)
238    {
239        let has_open_brace = line_contains_kind(input, SyntaxKind::LBrace);
240        let malformed = group_open_is_malformed(input);
241        consume_line(input);
242        return Ok(ParsedTopLevelItem::Namespace {
243            malformed,
244            is_close: false,
245            has_open_brace,
246        });
247    }
248
249    token_kind(input, SyntaxKind::LBracket)?;
250    let has_open_brace = line_contains_kind(input, SyntaxKind::LBrace);
251    let malformed = legacy_namespace_is_malformed(input);
252    consume_line(input);
253    Ok(ParsedTopLevelItem::Namespace {
254        malformed,
255        is_close: false,
256        has_open_brace,
257    })
258}
259
260fn group_open_is_malformed(input: &[SyntaxKind]) -> bool {
261    let line = input
262        .iter()
263        .copied()
264        .take_while(|kind| !matches!(kind, SyntaxKind::Newline | SyntaxKind::Eof))
265        .collect::<Vec<_>>();
266    let mut index = 0;
267    if line.get(index) != Some(&SyntaxKind::GroupKw) {
268        return true;
269    }
270    index += 1;
271    while line.get(index) == Some(&SyntaxKind::Whitespace) {
272        index += 1;
273    }
274    if line.get(index) != Some(&SyntaxKind::Ident) {
275        return true;
276    }
277    index += 1;
278    while line.get(index) == Some(&SyntaxKind::Whitespace) {
279        index += 1;
280    }
281    if line.get(index) != Some(&SyntaxKind::LBrace) {
282        return true;
283    }
284    index += 1;
285    while line.get(index) == Some(&SyntaxKind::Whitespace) {
286        index += 1;
287    }
288    index != line.len()
289}
290
291fn legacy_namespace_is_malformed(input: &[SyntaxKind]) -> bool {
292    let line = input
293        .iter()
294        .copied()
295        .take_while(|kind| !matches!(kind, SyntaxKind::Newline | SyntaxKind::Eof))
296        .collect::<Vec<_>>();
297    let mut index = 0;
298    while line.get(index) == Some(&SyntaxKind::Whitespace) {
299        index += 1;
300    }
301    if line.get(index) == Some(&SyntaxKind::Ident) {
302        index += 1;
303    }
304    while line.get(index) == Some(&SyntaxKind::Whitespace) {
305        index += 1;
306    }
307    if line.get(index) != Some(&SyntaxKind::RBracket) {
308        return true;
309    }
310    index += 1;
311    while line.get(index) == Some(&SyntaxKind::Whitespace) {
312        index += 1;
313    }
314    if index == line.len() {
315        return false;
316    }
317    if line.get(index) != Some(&SyntaxKind::LBrace) {
318        return true;
319    }
320    index += 1;
321    while line.get(index) == Some(&SyntaxKind::Whitespace) {
322        index += 1;
323    }
324    index != line.len()
325}
326
327fn parse_task_item(
328    input: &mut &[SyntaxKind],
329    in_braced_namespace: bool,
330) -> ModalResult<ParsedTopLevelItem> {
331    token_kind(input, SyntaxKind::Ident)?;
332    let mut saw_colon = false;
333    let mut header_complete = false;
334    let mut line_start = false;
335    let mut malformed = false;
336    let mut expect_guard_at = false;
337    let mut phase = TaskHeaderPhase::BeforeTail;
338    let mut saw_parameter_list = false;
339    let mut continuation_header = false;
340    let mut expect_clause_start = false;
341    let mut expect_param_indent = false;
342
343    while let Some(kind) = input.first().copied() {
344        if header_complete
345            && line_start
346            && (starts_top_level_item(kind)
347                || starts_indented_namespace_boundary(input)
348                || (in_braced_namespace && starts_indented_namespace_member(input)))
349        {
350            break;
351        }
352
353        if saw_colon
354            && !header_complete
355            && !matches!(kind, SyntaxKind::Whitespace | SyntaxKind::Newline)
356        {
357            malformed = true;
358        }
359
360        if !header_complete {
361            if expect_param_indent {
362                match kind {
363                    SyntaxKind::Indent => expect_param_indent = false,
364                    SyntaxKind::RParen => expect_param_indent = false,
365                    _ => {
366                        malformed = true;
367                        break;
368                    }
369                }
370            }
371
372            if kind == SyntaxKind::Comment {
373                malformed = true;
374            }
375
376            if continuation_header && expect_clause_start {
377                match kind {
378                    // Header indentation is formatting, not syntax. The first meaningful token
379                    // determines whether this line is a clause or the header terminator.
380                    SyntaxKind::Indent | SyntaxKind::Whitespace => {}
381                    SyntaxKind::Question
382                    | SyntaxKind::Amp
383                    | SyntaxKind::ShellKw
384                    | SyntaxKind::ShellFallbackKw => {
385                        expect_clause_start = false;
386                    }
387                    SyntaxKind::Colon => {
388                        expect_clause_start = false;
389                    }
390                    SyntaxKind::Newline => malformed = true,
391                    _ => {
392                        malformed = true;
393                        expect_clause_start = false;
394                    }
395                }
396            }
397
398            match &mut phase {
399                TaskHeaderPhase::BeforeTail => match kind {
400                    SyntaxKind::LParen => {
401                        saw_parameter_list = true;
402                        phase = TaskHeaderPhase::Params { depth: 1 };
403                    }
404                    SyntaxKind::Question => {
405                        phase = TaskHeaderPhase::Condition { depth: 0 };
406                        expect_guard_at = true;
407                    }
408                    SyntaxKind::Amp => {
409                        phase = TaskHeaderPhase::Dependencies {
410                            group_depth: 0,
411                            call_args: None,
412                            previous: Some(SyntaxKind::Amp),
413                        };
414                    }
415                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
416                    SyntaxKind::At if expect_guard_at => {
417                        expect_guard_at = false;
418                    }
419                    _ => {
420                        if expect_guard_at {
421                            malformed = true;
422                            expect_guard_at = false;
423                        }
424                    }
425                },
426                TaskHeaderPhase::Params { depth } => match kind {
427                    SyntaxKind::LParen => *depth += 1,
428                    SyntaxKind::RParen => {
429                        if *depth == 0 {
430                            malformed = true;
431                        } else {
432                            *depth -= 1;
433                            if *depth == 0 {
434                                phase = TaskHeaderPhase::BeforeTail;
435                            }
436                        }
437                    }
438                    _ => {}
439                },
440                TaskHeaderPhase::Condition { depth } => match kind {
441                    SyntaxKind::LParen => *depth += 1,
442                    SyntaxKind::RParen => {
443                        if *depth > 0 {
444                            *depth -= 1;
445                        }
446                        if *depth == 0 {
447                            phase = TaskHeaderPhase::BeforeTail;
448                        }
449                    }
450                    SyntaxKind::At if expect_guard_at => {
451                        expect_guard_at = false;
452                    }
453                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
454                    _ => {
455                        if expect_guard_at {
456                            malformed = true;
457                            expect_guard_at = false;
458                        }
459                    }
460                },
461                TaskHeaderPhase::Dependencies {
462                    group_depth,
463                    call_args,
464                    previous,
465                } => match kind {
466                    SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline => {}
467                    SyntaxKind::LParen => {
468                        if call_args.is_some() {
469                            malformed = true;
470                        } else if *previous == Some(SyntaxKind::Ident) {
471                            *call_args = Some(DependencyArgState::FirstOrEnd);
472                        } else {
473                            if *group_depth > 0 {
474                                malformed = true;
475                            }
476                            *group_depth += 1;
477                        }
478                        *previous = Some(kind);
479                    }
480                    SyntaxKind::RParen => {
481                        if let Some(argument_state) = call_args {
482                            if *argument_state == DependencyArgState::Value {
483                                malformed = true;
484                            }
485                            *call_args = None;
486                        } else if *group_depth > 0 {
487                            *group_depth -= 1;
488                        } else {
489                            malformed = true;
490                        }
491                        *previous = Some(kind);
492                    }
493                    SyntaxKind::String => {
494                        match call_args {
495                            Some(DependencyArgState::FirstOrEnd | DependencyArgState::Value) => {
496                                *call_args = Some(DependencyArgState::CommaOrEnd);
497                            }
498                            _ => malformed = true,
499                        }
500                        *previous = Some(kind);
501                    }
502                    SyntaxKind::Comma => {
503                        match call_args {
504                            Some(DependencyArgState::CommaOrEnd) => {
505                                *call_args = Some(DependencyArgState::Value);
506                            }
507                            Some(_) => malformed = true,
508                            None if *group_depth == 0 => malformed = true,
509                            None => {}
510                        }
511                        *previous = Some(kind);
512                    }
513                    SyntaxKind::Question | SyntaxKind::At => malformed = true,
514                    SyntaxKind::ShellKw | SyntaxKind::ShellFallbackKw
515                        if *group_depth == 0 && call_args.is_none() =>
516                    {
517                        phase = TaskHeaderPhase::Shell;
518                    }
519                    _ => {
520                        if call_args.is_some() {
521                            malformed = true;
522                        }
523                        *previous = Some(kind);
524                    }
525                },
526                TaskHeaderPhase::Shell => {}
527            }
528        }
529
530        if kind == SyntaxKind::Colon && phase.is_balanced() {
531            saw_colon = true;
532        }
533        advance(input);
534
535        if kind == SyntaxKind::Eof {
536            break;
537        }
538
539        if kind == SyntaxKind::Newline && !saw_colon {
540            if matches!(phase, TaskHeaderPhase::Params { depth } if depth > 0) {
541                expect_param_indent = true;
542                line_start = true;
543                continue;
544            }
545            if saw_parameter_list && phase.is_balanced() && !expect_guard_at {
546                continuation_header = true;
547                expect_clause_start = true;
548                line_start = true;
549                continue;
550            }
551            malformed |= !phase.is_balanced() || expect_guard_at;
552            break;
553        }
554
555        if kind == SyntaxKind::Newline && saw_colon {
556            malformed |= !phase.is_balanced() || expect_guard_at;
557            header_complete = true;
558        }
559
560        line_start = kind == SyntaxKind::Newline;
561    }
562
563    Ok(ParsedTopLevelItem::Task {
564        saw_colon,
565        malformed,
566    })
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
570enum TaskHeaderPhase {
571    BeforeTail,
572    Params {
573        depth: usize,
574    },
575    Condition {
576        depth: usize,
577    },
578    Dependencies {
579        group_depth: usize,
580        call_args: Option<DependencyArgState>,
581        previous: Option<SyntaxKind>,
582    },
583    Shell,
584}
585
586#[derive(Debug, Clone, Copy, PartialEq, Eq)]
587enum DependencyArgState {
588    FirstOrEnd,
589    Value,
590    CommaOrEnd,
591}
592
593impl TaskHeaderPhase {
594    fn is_balanced(self) -> bool {
595        match self {
596            TaskHeaderPhase::BeforeTail | TaskHeaderPhase::Shell => true,
597            TaskHeaderPhase::Params { depth } | TaskHeaderPhase::Condition { depth } => depth == 0,
598            TaskHeaderPhase::Dependencies {
599                group_depth,
600                call_args,
601                ..
602            } => group_depth == 0 && call_args.is_none(),
603        }
604    }
605}
606
607fn parse_unexpected_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
608    any::<_, ErrMode<ContextError>>
609        .verify(|kind: &SyntaxKind| !is_trivia(*kind) && *kind != SyntaxKind::Eof)
610        .value(ParsedTopLevelItem::Unexpected)
611        .parse_next(input)
612}
613
614fn token_kind(input: &mut &[SyntaxKind], kind: SyntaxKind) -> ModalResult<SyntaxKind> {
615    any::<_, ErrMode<ContextError>>
616        .verify(move |candidate: &SyntaxKind| *candidate == kind)
617        .parse_next(input)
618}
619
620fn parse_error(code: &str, message: &str, range: TextRange) -> Diagnostic {
621    Diagnostic::new(
622        DiagnosticSeverity::Error,
623        DiagnosticCode::new(code),
624        message,
625        DiagnosticPhase::Parse,
626        normalize_range(range),
627    )
628}
629
630fn normalize_range(range: TextRange) -> TextRange {
631    if range.is_empty() {
632        TextRange::new(range.start(), range.start() + TextSize::from(1))
633    } else {
634        range
635    }
636}