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 group",
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                malformed,
131                missing_colon,
132            } => {
133                if malformed {
134                    let (code, message) = if missing_colon {
135                        ("parse.missing-task-colon", "missing ':' before command")
136                    } else {
137                        ("parse.malformed-task-header", "invalid task header")
138                    };
139                    diagnostics.push(parse_error(code, message, token.range));
140                    builder.push_node(SyntaxKind::Error, token_slice);
141                    continue;
142                }
143                builder.push_task(token_slice);
144            }
145            ParsedTopLevelItem::Unexpected => {
146                diagnostics.push(parse_error(
147                    "parse.unexpected-token",
148                    "unexpected text",
149                    token.range,
150                ));
151                builder.push_node(SyntaxKind::Error, token_slice);
152            }
153        }
154    }
155
156    ParseResult {
157        root: builder.finish(),
158        diagnostics,
159    }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163enum ParsedTopLevelItem {
164    Directive {
165        malformed: bool,
166    },
167    MetadataComment,
168    Namespace {
169        malformed: bool,
170        is_close: bool,
171        has_open_brace: bool,
172    },
173    Task {
174        malformed: bool,
175        missing_colon: bool,
176    },
177    Unexpected,
178}
179
180fn parse_top_level_item(
181    input: &mut &[SyntaxKind],
182    in_braced_namespace: bool,
183) -> ModalResult<ParsedTopLevelItem> {
184    alt((
185        parse_directive_item,
186        parse_metadata_item,
187        parse_namespace_item,
188        |input: &mut &[SyntaxKind]| parse_task_item(input, in_braced_namespace),
189        parse_unexpected_item,
190    ))
191    .parse_next(input)
192}
193
194fn parse_directive_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
195    token_kind(input, SyntaxKind::Bang)?;
196    let malformed = !line_has_non_trivia(input) || line_contains_kind(input, SyntaxKind::Comment);
197    consume_line(input);
198    Ok(ParsedTopLevelItem::Directive { malformed })
199}
200
201fn parse_metadata_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
202    token_kind(input, SyntaxKind::LBracket)?;
203    token_kind(input, SyntaxKind::Ident)?;
204    token_kind(input, SyntaxKind::RBracket)?;
205
206    let mut rest = *input;
207    while matches!(rest.first(), Some(SyntaxKind::Whitespace)) {
208        advance(&mut rest);
209    }
210    let starts_namespace_brace =
211        rest.first() == Some(&SyntaxKind::LBrace) && rest.get(1) != Some(&SyntaxKind::LBrace);
212    let has_text = !matches!(rest.first(), Some(SyntaxKind::Newline | SyntaxKind::Eof))
213        && !starts_namespace_brace
214        && rest.first() != Some(&SyntaxKind::Comment);
215    if !has_text {
216        return Err(ErrMode::Backtrack(ContextError::new()));
217    }
218
219    consume_line(input);
220    Ok(ParsedTopLevelItem::MetadataComment)
221}
222
223fn parse_namespace_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
224    if input.first() == Some(&SyntaxKind::RBrace) {
225        advance(input);
226        let malformed =
227            line_has_non_trivia(input) || line_contains_kind(input, SyntaxKind::Comment);
228        consume_line(input);
229        return Ok(ParsedTopLevelItem::Namespace {
230            malformed,
231            is_close: true,
232            has_open_brace: false,
233        });
234    }
235
236    if input.first() == Some(&SyntaxKind::GroupKw)
237        && input.get(1) == Some(&SyntaxKind::Whitespace)
238        && input.get(2) == Some(&SyntaxKind::Ident)
239    {
240        let has_open_brace = line_contains_kind(input, SyntaxKind::LBrace);
241        let malformed = group_open_is_malformed(input);
242        consume_line(input);
243        return Ok(ParsedTopLevelItem::Namespace {
244            malformed,
245            is_close: false,
246            has_open_brace,
247        });
248    }
249
250    Err(ErrMode::Backtrack(ContextError::new()))
251}
252
253fn group_open_is_malformed(input: &[SyntaxKind]) -> bool {
254    let line = input
255        .iter()
256        .copied()
257        .take_while(|kind| !matches!(kind, SyntaxKind::Newline | SyntaxKind::Eof))
258        .collect::<Vec<_>>();
259    let mut index = 0;
260    if line.get(index) != Some(&SyntaxKind::GroupKw) {
261        return true;
262    }
263    index += 1;
264    while line.get(index) == Some(&SyntaxKind::Whitespace) {
265        index += 1;
266    }
267    if line.get(index) != Some(&SyntaxKind::Ident) {
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::LBrace) {
275        return true;
276    }
277    index += 1;
278    while line.get(index) == Some(&SyntaxKind::Whitespace) {
279        index += 1;
280    }
281    index != line.len()
282}
283
284fn parse_task_item(
285    input: &mut &[SyntaxKind],
286    in_braced_namespace: bool,
287) -> ModalResult<ParsedTopLevelItem> {
288    token_kind(input, SyntaxKind::Ident)?;
289    let mut saw_colon = false;
290    let mut header_complete = false;
291    let mut line_start = false;
292    let mut malformed = false;
293    let mut missing_colon = false;
294    let mut expect_guard_at = false;
295    let mut phase = TaskHeaderPhase::BeforeTail;
296    let mut saw_parameter_list = false;
297    let mut continuation_header = false;
298    let mut expect_clause_start = false;
299    let mut expect_param_indent = false;
300
301    while let Some(kind) = input.first().copied() {
302        if line_start
303            && (header_complete || (continuation_header && !saw_colon))
304            && (starts_top_level_item(kind)
305                || starts_indented_namespace_boundary(input)
306                || (in_braced_namespace && starts_indented_namespace_member(input)))
307        {
308            break;
309        }
310
311        // A blank line terminates a no-body header. It must not be treated as
312        // another multiline clause, otherwise a formatted dependency-only task
313        // would become malformed on the following declaration.
314        if continuation_header && !saw_colon && line_start && kind == SyntaxKind::Newline {
315            break;
316        }
317
318        if saw_colon
319            && !header_complete
320            && !matches!(
321                kind,
322                SyntaxKind::Whitespace | SyntaxKind::Newline | SyntaxKind::Eof
323            )
324        {
325            malformed = true;
326        }
327
328        if !header_complete {
329            if expect_param_indent {
330                match kind {
331                    SyntaxKind::Indent => expect_param_indent = false,
332                    SyntaxKind::RParen => expect_param_indent = false,
333                    _ => {
334                        malformed = true;
335                        break;
336                    }
337                }
338            }
339
340            if kind == SyntaxKind::Comment {
341                malformed = true;
342            }
343
344            if continuation_header && expect_clause_start {
345                match kind {
346                    // Header indentation is formatting, not syntax. The first meaningful token
347                    // determines whether this line is a clause or the header terminator.
348                    SyntaxKind::Indent | SyntaxKind::Whitespace => {}
349                    SyntaxKind::Question
350                    | SyntaxKind::Amp
351                    | SyntaxKind::ShellKw
352                    | SyntaxKind::ShellFallbackKw => {
353                        expect_clause_start = false;
354                    }
355                    SyntaxKind::Colon => {
356                        expect_clause_start = false;
357                    }
358                    SyntaxKind::Newline => malformed = true,
359                    SyntaxKind::Eof => {}
360                    _ => {
361                        malformed = true;
362                        missing_colon = true;
363                        expect_clause_start = false;
364                    }
365                }
366            }
367
368            match &mut phase {
369                TaskHeaderPhase::BeforeTail => match kind {
370                    SyntaxKind::LParen => {
371                        saw_parameter_list = true;
372                        phase = TaskHeaderPhase::Params { depth: 1 };
373                    }
374                    SyntaxKind::Question => {
375                        phase = TaskHeaderPhase::Condition { depth: 0 };
376                        expect_guard_at = true;
377                    }
378                    SyntaxKind::Amp => {
379                        phase = TaskHeaderPhase::Dependencies {
380                            group_depth: 0,
381                            call_args: None,
382                            previous: Some(SyntaxKind::Amp),
383                        };
384                    }
385                    SyntaxKind::ShellKw | SyntaxKind::ShellFallbackKw => {
386                        phase = TaskHeaderPhase::Shell;
387                    }
388                    SyntaxKind::Whitespace
389                    | SyntaxKind::Indent
390                    | SyntaxKind::Newline
391                    | SyntaxKind::Eof => {}
392                    SyntaxKind::At if expect_guard_at => {
393                        expect_guard_at = false;
394                    }
395                    SyntaxKind::Colon => {}
396                    _ => {
397                        malformed = true;
398                        if expect_guard_at {
399                            expect_guard_at = false;
400                        }
401                    }
402                },
403                TaskHeaderPhase::Params { depth } => match kind {
404                    SyntaxKind::LParen => *depth += 1,
405                    SyntaxKind::RParen => {
406                        if *depth == 0 {
407                            malformed = true;
408                        } else {
409                            *depth -= 1;
410                            if *depth == 0 {
411                                phase = TaskHeaderPhase::BeforeTail;
412                            }
413                        }
414                    }
415                    _ => {}
416                },
417                TaskHeaderPhase::Condition { depth } => match kind {
418                    SyntaxKind::LParen => *depth += 1,
419                    SyntaxKind::RParen => {
420                        if *depth > 0 {
421                            *depth -= 1;
422                        }
423                        if *depth == 0 {
424                            phase = TaskHeaderPhase::BeforeTail;
425                        }
426                    }
427                    SyntaxKind::At if expect_guard_at => {
428                        expect_guard_at = false;
429                    }
430                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
431                    _ => {
432                        if expect_guard_at {
433                            malformed = true;
434                            expect_guard_at = false;
435                        }
436                    }
437                },
438                TaskHeaderPhase::Dependencies {
439                    group_depth,
440                    call_args,
441                    previous,
442                } => match kind {
443                    SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline => {}
444                    SyntaxKind::LParen => {
445                        if call_args.is_some() {
446                            malformed = true;
447                        } else if *previous == Some(SyntaxKind::Ident) {
448                            *call_args = Some(DependencyArgState::FirstOrEnd);
449                        } else {
450                            if *group_depth > 0 {
451                                malformed = true;
452                            }
453                            *group_depth += 1;
454                        }
455                        *previous = Some(kind);
456                    }
457                    SyntaxKind::RParen => {
458                        if let Some(argument_state) = call_args {
459                            if *argument_state == DependencyArgState::Value {
460                                malformed = true;
461                            }
462                            *call_args = None;
463                        } else if *group_depth > 0 {
464                            *group_depth -= 1;
465                        } else {
466                            malformed = true;
467                        }
468                        *previous = Some(kind);
469                    }
470                    SyntaxKind::String => {
471                        match call_args {
472                            Some(DependencyArgState::FirstOrEnd | DependencyArgState::Value) => {
473                                *call_args = Some(DependencyArgState::CommaOrEnd);
474                            }
475                            _ => malformed = true,
476                        }
477                        *previous = Some(kind);
478                    }
479                    SyntaxKind::Comma => {
480                        match call_args {
481                            Some(DependencyArgState::CommaOrEnd) => {
482                                *call_args = Some(DependencyArgState::Value);
483                            }
484                            Some(_) => malformed = true,
485                            None if *group_depth == 0 => malformed = true,
486                            None => {}
487                        }
488                        *previous = Some(kind);
489                    }
490                    SyntaxKind::Question | SyntaxKind::At => malformed = true,
491                    SyntaxKind::ShellKw | SyntaxKind::ShellFallbackKw
492                        if *group_depth == 0 && call_args.is_none() =>
493                    {
494                        phase = TaskHeaderPhase::Shell;
495                    }
496                    _ => {
497                        if call_args.is_some() {
498                            malformed = true;
499                        }
500                        *previous = Some(kind);
501                    }
502                },
503                TaskHeaderPhase::Shell => {
504                    if kind == SyntaxKind::Question {
505                        malformed = true;
506                    }
507                }
508            }
509        }
510
511        if kind == SyntaxKind::Colon && phase.is_balanced() {
512            saw_colon = true;
513        }
514        advance(input);
515
516        if kind == SyntaxKind::Eof {
517            malformed |= !phase.is_balanced() || expect_guard_at;
518            break;
519        }
520
521        if kind == SyntaxKind::Newline && !saw_colon {
522            if matches!(phase, TaskHeaderPhase::Params { depth } if depth > 0) {
523                expect_param_indent = true;
524                line_start = true;
525                continue;
526            }
527            if saw_parameter_list && phase.is_balanced() && !expect_guard_at {
528                continuation_header = true;
529                expect_clause_start = true;
530                line_start = true;
531                continue;
532            }
533            malformed |= !phase.is_balanced() || expect_guard_at;
534            break;
535        }
536
537        if kind == SyntaxKind::Newline && saw_colon {
538            malformed |= !phase.is_balanced() || expect_guard_at;
539            header_complete = true;
540        }
541
542        line_start = kind == SyntaxKind::Newline;
543    }
544
545    Ok(ParsedTopLevelItem::Task {
546        malformed,
547        missing_colon,
548    })
549}
550
551#[derive(Debug, Clone, Copy, PartialEq, Eq)]
552enum TaskHeaderPhase {
553    BeforeTail,
554    Params {
555        depth: usize,
556    },
557    Condition {
558        depth: usize,
559    },
560    Dependencies {
561        group_depth: usize,
562        call_args: Option<DependencyArgState>,
563        previous: Option<SyntaxKind>,
564    },
565    Shell,
566}
567
568#[derive(Debug, Clone, Copy, PartialEq, Eq)]
569enum DependencyArgState {
570    FirstOrEnd,
571    Value,
572    CommaOrEnd,
573}
574
575impl TaskHeaderPhase {
576    fn is_balanced(self) -> bool {
577        match self {
578            TaskHeaderPhase::BeforeTail | TaskHeaderPhase::Shell => true,
579            TaskHeaderPhase::Params { depth } | TaskHeaderPhase::Condition { depth } => depth == 0,
580            TaskHeaderPhase::Dependencies {
581                group_depth,
582                call_args,
583                ..
584            } => group_depth == 0 && call_args.is_none(),
585        }
586    }
587}
588
589fn parse_unexpected_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
590    any::<_, ErrMode<ContextError>>
591        .verify(|kind: &SyntaxKind| !is_trivia(*kind) && *kind != SyntaxKind::Eof)
592        .value(ParsedTopLevelItem::Unexpected)
593        .parse_next(input)
594}
595
596fn token_kind(input: &mut &[SyntaxKind], kind: SyntaxKind) -> ModalResult<SyntaxKind> {
597    any::<_, ErrMode<ContextError>>
598        .verify(move |candidate: &SyntaxKind| *candidate == kind)
599        .parse_next(input)
600}
601
602fn parse_error(code: &str, message: &str, range: TextRange) -> Diagnostic {
603    Diagnostic::new(
604        DiagnosticSeverity::Error,
605        DiagnosticCode::new(code),
606        message,
607        DiagnosticPhase::Parse,
608        normalize_range(range),
609    )
610}
611
612fn normalize_range(range: TextRange) -> TextRange {
613    if range.is_empty() {
614        TextRange::new(range.start(), range.start() + TextSize::from(1))
615    } else {
616        range
617    }
618}