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::DocComment => {
110                builder.push_node(SyntaxKind::DocComment, 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    DocComment,
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_doc_comment_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_doc_comment_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
201    token_kind(input, SyntaxKind::Percent)?;
202    consume_line(input);
203    Ok(ParsedTopLevelItem::DocComment)
204}
205
206fn parse_namespace_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
207    if input.first() == Some(&SyntaxKind::RBrace) {
208        advance(input);
209        let malformed =
210            line_has_non_trivia(input) || line_contains_kind(input, SyntaxKind::Comment);
211        consume_line(input);
212        return Ok(ParsedTopLevelItem::Namespace {
213            malformed,
214            is_close: true,
215            has_open_brace: false,
216        });
217    }
218
219    token_kind(input, SyntaxKind::LBracket)?;
220    let has_open_brace = line_contains_kind(input, SyntaxKind::LBrace);
221    let malformed = namespace_open_is_malformed(input);
222    consume_line(input);
223    Ok(ParsedTopLevelItem::Namespace {
224        malformed,
225        is_close: false,
226        has_open_brace,
227    })
228}
229
230fn namespace_open_is_malformed(input: &[SyntaxKind]) -> bool {
231    let line = input
232        .iter()
233        .copied()
234        .take_while(|kind| !matches!(kind, SyntaxKind::Newline | SyntaxKind::Eof))
235        .collect::<Vec<_>>();
236    let mut index = 0;
237
238    while line.get(index) == Some(&SyntaxKind::Whitespace) {
239        index += 1;
240    }
241    if line.get(index) == Some(&SyntaxKind::Ident) {
242        index += 1;
243    }
244    while line.get(index) == Some(&SyntaxKind::Whitespace) {
245        index += 1;
246    }
247    if line.get(index) != Some(&SyntaxKind::RBracket) {
248        return true;
249    }
250    index += 1;
251    while line.get(index) == Some(&SyntaxKind::Whitespace) {
252        index += 1;
253    }
254    if index == line.len() {
255        return false;
256    }
257    if line.get(index) != Some(&SyntaxKind::LBrace) {
258        return true;
259    }
260    index += 1;
261    while line.get(index) == Some(&SyntaxKind::Whitespace) {
262        index += 1;
263    }
264    index != line.len()
265}
266
267fn parse_task_item(
268    input: &mut &[SyntaxKind],
269    in_braced_namespace: bool,
270) -> ModalResult<ParsedTopLevelItem> {
271    token_kind(input, SyntaxKind::Ident)?;
272    let mut saw_colon = false;
273    let mut header_complete = false;
274    let mut line_start = false;
275    let mut malformed = false;
276    let mut expect_guard_at = false;
277    let mut phase = TaskHeaderPhase::BeforeTail;
278    let mut saw_parameter_list = false;
279    let mut continuation_header = false;
280    let mut expect_clause_start = false;
281    let mut continuation_indent = false;
282    let mut expect_param_indent = false;
283
284    while let Some(kind) = input.first().copied() {
285        if header_complete
286            && line_start
287            && (starts_top_level_item(kind)
288                || starts_indented_namespace_boundary(input)
289                || (in_braced_namespace && starts_indented_namespace_member(input)))
290        {
291            break;
292        }
293
294        if saw_colon
295            && !header_complete
296            && !matches!(kind, SyntaxKind::Whitespace | SyntaxKind::Newline)
297        {
298            malformed = true;
299        }
300
301        if !header_complete {
302            if expect_param_indent {
303                match kind {
304                    SyntaxKind::Indent => expect_param_indent = false,
305                    SyntaxKind::RParen => expect_param_indent = false,
306                    _ => {
307                        malformed = true;
308                        break;
309                    }
310                }
311            }
312
313            if kind == SyntaxKind::Comment {
314                malformed = true;
315            }
316
317            if continuation_header && expect_clause_start {
318                match kind {
319                    SyntaxKind::Indent | SyntaxKind::Whitespace => {
320                        continuation_indent = true;
321                    }
322                    SyntaxKind::Question
323                    | SyntaxKind::Amp
324                    | SyntaxKind::ShellKw
325                    | SyntaxKind::ShellFallbackKw => {
326                        malformed |= !continuation_indent;
327                        expect_clause_start = false;
328                    }
329                    SyntaxKind::Colon => {
330                        malformed |= continuation_indent;
331                        expect_clause_start = false;
332                    }
333                    SyntaxKind::Newline => malformed = true,
334                    _ => {
335                        malformed = true;
336                        expect_clause_start = false;
337                    }
338                }
339            }
340
341            match &mut phase {
342                TaskHeaderPhase::BeforeTail => match kind {
343                    SyntaxKind::LParen => {
344                        saw_parameter_list = true;
345                        phase = TaskHeaderPhase::Params { depth: 1 };
346                    }
347                    SyntaxKind::Question => {
348                        phase = TaskHeaderPhase::Guard { depth: 0 };
349                        expect_guard_at = true;
350                    }
351                    SyntaxKind::Amp => {
352                        phase = TaskHeaderPhase::Dependencies {
353                            group_depth: 0,
354                            saw_group: false,
355                        };
356                    }
357                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
358                    SyntaxKind::At if expect_guard_at => {
359                        expect_guard_at = false;
360                    }
361                    _ => {
362                        if expect_guard_at {
363                            malformed = true;
364                            expect_guard_at = false;
365                        }
366                    }
367                },
368                TaskHeaderPhase::Params { depth } => match kind {
369                    SyntaxKind::LParen => *depth += 1,
370                    SyntaxKind::RParen => {
371                        if *depth == 0 {
372                            malformed = true;
373                        } else {
374                            *depth -= 1;
375                            if *depth == 0 {
376                                phase = TaskHeaderPhase::BeforeTail;
377                            }
378                        }
379                    }
380                    _ => {}
381                },
382                TaskHeaderPhase::Guard { depth } => match kind {
383                    SyntaxKind::LParen => *depth += 1,
384                    SyntaxKind::RParen => {
385                        if *depth > 0 {
386                            *depth -= 1;
387                        }
388                        if *depth == 0 {
389                            phase = TaskHeaderPhase::BeforeTail;
390                        }
391                    }
392                    SyntaxKind::At if expect_guard_at => {
393                        expect_guard_at = false;
394                    }
395                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
396                    _ => {
397                        if expect_guard_at {
398                            malformed = true;
399                            expect_guard_at = false;
400                        }
401                    }
402                },
403                TaskHeaderPhase::Dependencies {
404                    group_depth,
405                    saw_group,
406                } => match kind {
407                    SyntaxKind::LParen => {
408                        if *group_depth > 0 {
409                            malformed = true;
410                        }
411                        *group_depth += 1;
412                        *saw_group = true;
413                    }
414                    SyntaxKind::RParen => {
415                        if *group_depth == 0 {
416                            malformed = true;
417                        } else {
418                            *group_depth -= 1;
419                        }
420                    }
421                    SyntaxKind::Question | SyntaxKind::At => malformed = true,
422                    SyntaxKind::ShellKw | SyntaxKind::ShellFallbackKw if *group_depth == 0 => {
423                        phase = TaskHeaderPhase::Shell;
424                    }
425                    SyntaxKind::Unknown if kind == SyntaxKind::Unknown => {}
426                    _ => {}
427                },
428                TaskHeaderPhase::Shell => {}
429            }
430        }
431
432        if kind == SyntaxKind::Colon && phase.is_balanced() {
433            saw_colon = true;
434        }
435        advance(input);
436
437        if kind == SyntaxKind::Eof {
438            break;
439        }
440
441        if kind == SyntaxKind::Newline && !saw_colon {
442            if matches!(phase, TaskHeaderPhase::Params { depth } if depth > 0) {
443                expect_param_indent = true;
444                line_start = true;
445                continue;
446            }
447            if saw_parameter_list && phase.is_balanced() && !expect_guard_at {
448                continuation_header = true;
449                expect_clause_start = true;
450                continuation_indent = false;
451                line_start = true;
452                continue;
453            }
454            malformed |= !phase.is_balanced() || expect_guard_at;
455            break;
456        }
457
458        if kind == SyntaxKind::Newline && saw_colon {
459            malformed |= !phase.is_balanced() || expect_guard_at;
460            header_complete = true;
461        }
462
463        line_start = kind == SyntaxKind::Newline;
464    }
465
466    Ok(ParsedTopLevelItem::Task {
467        saw_colon,
468        malformed,
469    })
470}
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq)]
473enum TaskHeaderPhase {
474    BeforeTail,
475    Params { depth: usize },
476    Guard { depth: usize },
477    Dependencies { group_depth: usize, saw_group: bool },
478    Shell,
479}
480
481impl TaskHeaderPhase {
482    fn is_balanced(self) -> bool {
483        match self {
484            TaskHeaderPhase::BeforeTail | TaskHeaderPhase::Shell => true,
485            TaskHeaderPhase::Params { depth } | TaskHeaderPhase::Guard { depth } => depth == 0,
486            TaskHeaderPhase::Dependencies { group_depth, .. } => group_depth == 0,
487        }
488    }
489}
490
491fn parse_unexpected_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
492    any::<_, ErrMode<ContextError>>
493        .verify(|kind: &SyntaxKind| !is_trivia(*kind) && *kind != SyntaxKind::Eof)
494        .value(ParsedTopLevelItem::Unexpected)
495        .parse_next(input)
496}
497
498fn token_kind(input: &mut &[SyntaxKind], kind: SyntaxKind) -> ModalResult<SyntaxKind> {
499    any::<_, ErrMode<ContextError>>
500        .verify(move |candidate: &SyntaxKind| *candidate == kind)
501        .parse_next(input)
502}
503
504fn parse_error(code: &str, message: &str, range: TextRange) -> Diagnostic {
505    Diagnostic::new(
506        DiagnosticSeverity::Error,
507        DiagnosticCode::new(code),
508        message,
509        DiagnosticPhase::Parse,
510        normalize_range(range),
511    )
512}
513
514fn normalize_range(range: TextRange) -> TextRange {
515    if range.is_empty() {
516        TextRange::new(range.start(), range.start() + TextSize::from(1))
517    } else {
518        range
519    }
520}