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 expect_param_indent = false;
282
283    while let Some(kind) = input.first().copied() {
284        if header_complete
285            && line_start
286            && (starts_top_level_item(kind)
287                || starts_indented_namespace_boundary(input)
288                || (in_braced_namespace && starts_indented_namespace_member(input)))
289        {
290            break;
291        }
292
293        if saw_colon
294            && !header_complete
295            && !matches!(kind, SyntaxKind::Whitespace | SyntaxKind::Newline)
296        {
297            malformed = true;
298        }
299
300        if !header_complete {
301            if expect_param_indent {
302                match kind {
303                    SyntaxKind::Indent => expect_param_indent = false,
304                    SyntaxKind::RParen => expect_param_indent = false,
305                    _ => {
306                        malformed = true;
307                        break;
308                    }
309                }
310            }
311
312            if kind == SyntaxKind::Comment {
313                malformed = true;
314            }
315
316            if continuation_header && expect_clause_start {
317                match kind {
318                    // Header indentation is formatting, not syntax. The first meaningful token
319                    // determines whether this line is a clause or the header terminator.
320                    SyntaxKind::Indent | SyntaxKind::Whitespace => {}
321                    SyntaxKind::Question
322                    | SyntaxKind::Amp
323                    | SyntaxKind::ShellKw
324                    | SyntaxKind::ShellFallbackKw => {
325                        expect_clause_start = false;
326                    }
327                    SyntaxKind::Colon => {
328                        expect_clause_start = false;
329                    }
330                    SyntaxKind::Newline => malformed = true,
331                    _ => {
332                        malformed = true;
333                        expect_clause_start = false;
334                    }
335                }
336            }
337
338            match &mut phase {
339                TaskHeaderPhase::BeforeTail => match kind {
340                    SyntaxKind::LParen => {
341                        saw_parameter_list = true;
342                        phase = TaskHeaderPhase::Params { depth: 1 };
343                    }
344                    SyntaxKind::Question => {
345                        phase = TaskHeaderPhase::Guard { depth: 0 };
346                        expect_guard_at = true;
347                    }
348                    SyntaxKind::Amp => {
349                        phase = TaskHeaderPhase::Dependencies {
350                            group_depth: 0,
351                            saw_group: false,
352                        };
353                    }
354                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
355                    SyntaxKind::At if expect_guard_at => {
356                        expect_guard_at = false;
357                    }
358                    _ => {
359                        if expect_guard_at {
360                            malformed = true;
361                            expect_guard_at = false;
362                        }
363                    }
364                },
365                TaskHeaderPhase::Params { depth } => match kind {
366                    SyntaxKind::LParen => *depth += 1,
367                    SyntaxKind::RParen => {
368                        if *depth == 0 {
369                            malformed = true;
370                        } else {
371                            *depth -= 1;
372                            if *depth == 0 {
373                                phase = TaskHeaderPhase::BeforeTail;
374                            }
375                        }
376                    }
377                    _ => {}
378                },
379                TaskHeaderPhase::Guard { depth } => match kind {
380                    SyntaxKind::LParen => *depth += 1,
381                    SyntaxKind::RParen => {
382                        if *depth > 0 {
383                            *depth -= 1;
384                        }
385                        if *depth == 0 {
386                            phase = TaskHeaderPhase::BeforeTail;
387                        }
388                    }
389                    SyntaxKind::At if expect_guard_at => {
390                        expect_guard_at = false;
391                    }
392                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
393                    _ => {
394                        if expect_guard_at {
395                            malformed = true;
396                            expect_guard_at = false;
397                        }
398                    }
399                },
400                TaskHeaderPhase::Dependencies {
401                    group_depth,
402                    saw_group,
403                } => match kind {
404                    SyntaxKind::LParen => {
405                        if *group_depth > 0 {
406                            malformed = true;
407                        }
408                        *group_depth += 1;
409                        *saw_group = true;
410                    }
411                    SyntaxKind::RParen => {
412                        if *group_depth == 0 {
413                            malformed = true;
414                        } else {
415                            *group_depth -= 1;
416                        }
417                    }
418                    SyntaxKind::Question | SyntaxKind::At => malformed = true,
419                    SyntaxKind::ShellKw | SyntaxKind::ShellFallbackKw if *group_depth == 0 => {
420                        phase = TaskHeaderPhase::Shell;
421                    }
422                    SyntaxKind::Unknown if kind == SyntaxKind::Unknown => {}
423                    _ => {}
424                },
425                TaskHeaderPhase::Shell => {}
426            }
427        }
428
429        if kind == SyntaxKind::Colon && phase.is_balanced() {
430            saw_colon = true;
431        }
432        advance(input);
433
434        if kind == SyntaxKind::Eof {
435            break;
436        }
437
438        if kind == SyntaxKind::Newline && !saw_colon {
439            if matches!(phase, TaskHeaderPhase::Params { depth } if depth > 0) {
440                expect_param_indent = true;
441                line_start = true;
442                continue;
443            }
444            if saw_parameter_list && phase.is_balanced() && !expect_guard_at {
445                continuation_header = true;
446                expect_clause_start = true;
447                line_start = true;
448                continue;
449            }
450            malformed |= !phase.is_balanced() || expect_guard_at;
451            break;
452        }
453
454        if kind == SyntaxKind::Newline && saw_colon {
455            malformed |= !phase.is_balanced() || expect_guard_at;
456            header_complete = true;
457        }
458
459        line_start = kind == SyntaxKind::Newline;
460    }
461
462    Ok(ParsedTopLevelItem::Task {
463        saw_colon,
464        malformed,
465    })
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469enum TaskHeaderPhase {
470    BeforeTail,
471    Params { depth: usize },
472    Guard { depth: usize },
473    Dependencies { group_depth: usize, saw_group: bool },
474    Shell,
475}
476
477impl TaskHeaderPhase {
478    fn is_balanced(self) -> bool {
479        match self {
480            TaskHeaderPhase::BeforeTail | TaskHeaderPhase::Shell => true,
481            TaskHeaderPhase::Params { depth } | TaskHeaderPhase::Guard { depth } => depth == 0,
482            TaskHeaderPhase::Dependencies { group_depth, .. } => group_depth == 0,
483        }
484    }
485}
486
487fn parse_unexpected_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
488    any::<_, ErrMode<ContextError>>
489        .verify(|kind: &SyntaxKind| !is_trivia(*kind) && *kind != SyntaxKind::Eof)
490        .value(ParsedTopLevelItem::Unexpected)
491        .parse_next(input)
492}
493
494fn token_kind(input: &mut &[SyntaxKind], kind: SyntaxKind) -> ModalResult<SyntaxKind> {
495    any::<_, ErrMode<ContextError>>
496        .verify(move |candidate: &SyntaxKind| *candidate == kind)
497        .parse_next(input)
498}
499
500fn parse_error(code: &str, message: &str, range: TextRange) -> Diagnostic {
501    Diagnostic::new(
502        DiagnosticSeverity::Error,
503        DiagnosticCode::new(code),
504        message,
505        DiagnosticPhase::Parse,
506        normalize_range(range),
507    )
508}
509
510fn normalize_range(range: TextRange) -> TextRange {
511    if range.is_empty() {
512        TextRange::new(range.start(), range.start() + TextSize::from(1))
513    } else {
514        range
515    }
516}