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::{advance, consume_line, starts_top_level_item};
14use crate::trivia::{is_trivia, line_contains_kind, line_has_non_trivia};
15use crate::{LexToken, SyntaxKind, lex};
16
17#[derive(Debug, Clone)]
18pub struct ParseResult {
19    pub root: SyntaxNode,
20    diagnostics: Vec<Diagnostic>,
21}
22
23impl ParseResult {
24    /// Returns the typed document CST root.
25    ///
26    /// Args:
27    /// None.
28    ///
29    /// Returns:
30    /// Typed document wrapper for the parse root.
31    pub fn document(&self) -> DocumentNode {
32        DocumentNode::cast(self.root.clone()).expect("parse root must always be a document node")
33    }
34}
35
36/// Extension helpers for parse results used by hosts and tests.
37pub trait ParseResultExt {
38    /// Returns root CST children for top-level inspection.
39    fn root_children(&self) -> SyntaxNodeChildren<crate::cst::OnlyLanguage>;
40
41    /// Returns collected parse diagnostics.
42    fn diagnostics(&self) -> &[Diagnostic];
43}
44
45impl ParseResultExt for ParseResult {
46    fn root_children(&self) -> SyntaxNodeChildren<crate::cst::OnlyLanguage> {
47        self.root.children()
48    }
49
50    fn diagnostics(&self) -> &[Diagnostic] {
51        &self.diagnostics
52    }
53}
54
55/// Parses Onlyfile text into a shallow CST with line-level recovery.
56///
57/// Args:
58/// source: Raw Onlyfile source text.
59///
60/// Returns:
61/// Parse result containing CST root and collected diagnostics.
62pub fn parse(source: &str) -> ParseResult {
63    let tokens = lex(source);
64    parse_tokens(&tokens)
65}
66
67pub(crate) fn parse_tokens(tokens: &[LexToken]) -> ParseResult {
68    let mut builder = ParseTreeBuilder::new();
69    let mut diagnostics = Vec::new();
70    let kinds = tokens.iter().map(|token| token.kind).collect::<Vec<_>>();
71    let mut cursor = TokenCursor::new(tokens, &kinds);
72
73    loop {
74        let trivia = cursor.skip_trivia();
75        builder.push_tokens(trivia);
76
77        let Some(token) = cursor.current() else {
78            break;
79        };
80        if token.kind == SyntaxKind::Eof {
81            break;
82        }
83
84        let mut input = cursor.remaining();
85        let (item, consumed) = parse_top_level_item
86            .with_taken()
87            .parse_next(&mut input)
88            .expect("top-level parser should always consume a non-EOF item");
89        let token_slice = cursor.consume(consumed.len());
90
91        match item {
92            ParsedTopLevelItem::Directive { malformed } => {
93                if malformed {
94                    diagnostics.push(parse_error(
95                        "parse.malformed-directive",
96                        "malformed directive",
97                        token.range,
98                    ));
99                    builder.push_node(SyntaxKind::Error, token_slice);
100                    continue;
101                }
102                builder.push_node(SyntaxKind::Directive, token_slice);
103            }
104            ParsedTopLevelItem::DocComment => {
105                builder.push_node(SyntaxKind::DocComment, token_slice);
106            }
107            ParsedTopLevelItem::Namespace { malformed } => {
108                if malformed {
109                    diagnostics.push(parse_error(
110                        "parse.malformed-namespace-header",
111                        "malformed namespace header",
112                        token.range,
113                    ));
114                    builder.push_node(SyntaxKind::Error, token_slice);
115                    continue;
116                }
117                builder.push_node(SyntaxKind::NamespaceBlock, token_slice);
118            }
119            ParsedTopLevelItem::Task {
120                saw_colon,
121                malformed,
122            } => {
123                if !saw_colon || malformed {
124                    diagnostics.push(parse_error(
125                        "parse.malformed-task-header",
126                        "malformed task header",
127                        token.range,
128                    ));
129                    builder.push_node(SyntaxKind::Error, token_slice);
130                    continue;
131                }
132                builder.push_node(SyntaxKind::TaskDecl, token_slice);
133            }
134            ParsedTopLevelItem::Unexpected => {
135                diagnostics.push(parse_error(
136                    "parse.unexpected-token",
137                    "unexpected top-level token",
138                    token.range,
139                ));
140                builder.push_node(SyntaxKind::Error, token_slice);
141            }
142        }
143    }
144
145    ParseResult {
146        root: builder.finish(),
147        diagnostics,
148    }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152enum ParsedTopLevelItem {
153    Directive { malformed: bool },
154    DocComment,
155    Namespace { malformed: bool },
156    Task { saw_colon: bool, malformed: bool },
157    Unexpected,
158}
159
160fn parse_top_level_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
161    alt((
162        parse_directive_item,
163        parse_doc_comment_item,
164        parse_namespace_item,
165        parse_task_item,
166        parse_unexpected_item,
167    ))
168    .parse_next(input)
169}
170
171fn parse_directive_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
172    token_kind(input, SyntaxKind::Bang)?;
173    let malformed = !line_has_non_trivia(input);
174    consume_line(input);
175    Ok(ParsedTopLevelItem::Directive { malformed })
176}
177
178fn parse_doc_comment_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
179    token_kind(input, SyntaxKind::Percent)?;
180    consume_line(input);
181    Ok(ParsedTopLevelItem::DocComment)
182}
183
184fn parse_namespace_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
185    token_kind(input, SyntaxKind::LBracket)?;
186    let malformed = !line_contains_kind(input, SyntaxKind::RBracket);
187    consume_line(input);
188    Ok(ParsedTopLevelItem::Namespace { malformed })
189}
190
191fn parse_task_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
192    token_kind(input, SyntaxKind::Ident)?;
193    let mut saw_colon = false;
194    let mut header_complete = false;
195    let mut line_start = false;
196    let mut malformed = false;
197    let mut expect_guard_at = false;
198    let mut phase = TaskHeaderPhase::BeforeTail;
199
200    while let Some(kind) = input.first().copied() {
201        if header_complete && line_start && starts_top_level_item(kind) {
202            break;
203        }
204
205        if !header_complete {
206            match &mut phase {
207                TaskHeaderPhase::BeforeTail => match kind {
208                    SyntaxKind::LParen => {
209                        phase = TaskHeaderPhase::Params { depth: 1 };
210                    }
211                    SyntaxKind::Question => {
212                        phase = TaskHeaderPhase::Guard { depth: 0 };
213                        expect_guard_at = true;
214                    }
215                    SyntaxKind::Amp => {
216                        phase = TaskHeaderPhase::Dependencies {
217                            group_depth: 0,
218                            saw_group: false,
219                        };
220                    }
221                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
222                    SyntaxKind::At if expect_guard_at => {
223                        expect_guard_at = false;
224                    }
225                    _ => {
226                        if expect_guard_at {
227                            malformed = true;
228                            expect_guard_at = false;
229                        }
230                    }
231                },
232                TaskHeaderPhase::Params { depth } => match kind {
233                    SyntaxKind::LParen => *depth += 1,
234                    SyntaxKind::RParen => {
235                        if *depth == 0 {
236                            malformed = true;
237                        } else {
238                            *depth -= 1;
239                            if *depth == 0 {
240                                phase = TaskHeaderPhase::BeforeTail;
241                            }
242                        }
243                    }
244                    _ => {}
245                },
246                TaskHeaderPhase::Guard { depth } => match kind {
247                    SyntaxKind::LParen => *depth += 1,
248                    SyntaxKind::RParen => {
249                        if *depth > 0 {
250                            *depth -= 1;
251                        }
252                        if *depth == 0 {
253                            phase = TaskHeaderPhase::BeforeTail;
254                        }
255                    }
256                    SyntaxKind::At if expect_guard_at => {
257                        expect_guard_at = false;
258                    }
259                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
260                    _ => {
261                        if expect_guard_at {
262                            malformed = true;
263                            expect_guard_at = false;
264                        }
265                    }
266                },
267                TaskHeaderPhase::Dependencies {
268                    group_depth,
269                    saw_group,
270                } => match kind {
271                    SyntaxKind::LParen => {
272                        if *group_depth > 0 {
273                            malformed = true;
274                        }
275                        *group_depth += 1;
276                        *saw_group = true;
277                    }
278                    SyntaxKind::RParen => {
279                        if *group_depth == 0 {
280                            malformed = true;
281                        } else {
282                            *group_depth -= 1;
283                        }
284                    }
285                    SyntaxKind::Question | SyntaxKind::At => malformed = true,
286                    SyntaxKind::ShellKw | SyntaxKind::ShellFallbackKw if *group_depth == 0 => {
287                        phase = TaskHeaderPhase::Shell;
288                    }
289                    SyntaxKind::Unknown if kind == SyntaxKind::Unknown => {}
290                    _ => {}
291                },
292                TaskHeaderPhase::Shell => {}
293            }
294        }
295
296        if kind == SyntaxKind::Colon {
297            saw_colon = true;
298        }
299        advance(input);
300
301        if kind == SyntaxKind::Eof {
302            break;
303        }
304
305        if kind == SyntaxKind::Newline && !saw_colon {
306            malformed |= !phase.is_balanced() || expect_guard_at;
307            break;
308        }
309
310        if kind == SyntaxKind::Newline && saw_colon {
311            malformed |= !phase.is_balanced() || expect_guard_at;
312            header_complete = true;
313        }
314
315        line_start = kind == SyntaxKind::Newline;
316    }
317
318    Ok(ParsedTopLevelItem::Task {
319        saw_colon,
320        malformed,
321    })
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325enum TaskHeaderPhase {
326    BeforeTail,
327    Params { depth: usize },
328    Guard { depth: usize },
329    Dependencies { group_depth: usize, saw_group: bool },
330    Shell,
331}
332
333impl TaskHeaderPhase {
334    fn is_balanced(self) -> bool {
335        match self {
336            TaskHeaderPhase::BeforeTail | TaskHeaderPhase::Shell => true,
337            TaskHeaderPhase::Params { depth } | TaskHeaderPhase::Guard { depth } => depth == 0,
338            TaskHeaderPhase::Dependencies { group_depth, .. } => group_depth == 0,
339        }
340    }
341}
342
343fn parse_unexpected_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
344    any::<_, ErrMode<ContextError>>
345        .verify(|kind: &SyntaxKind| !is_trivia(*kind) && *kind != SyntaxKind::Eof)
346        .value(ParsedTopLevelItem::Unexpected)
347        .parse_next(input)
348}
349
350fn token_kind(input: &mut &[SyntaxKind], kind: SyntaxKind) -> ModalResult<SyntaxKind> {
351    any::<_, ErrMode<ContextError>>
352        .verify(move |candidate: &SyntaxKind| *candidate == kind)
353        .parse_next(input)
354}
355
356fn parse_error(code: &str, message: &str, range: TextRange) -> Diagnostic {
357    Diagnostic::new(
358        DiagnosticSeverity::Error,
359        DiagnosticCode::new(code),
360        message,
361        DiagnosticPhase::Parse,
362        normalize_range(range),
363    )
364}
365
366fn normalize_range(range: TextRange) -> TextRange {
367    if range.is_empty() {
368        TextRange::new(range.start(), range.start() + TextSize::from(1))
369    } else {
370        range
371    }
372}