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) || line_contains_kind(input, SyntaxKind::Comment);
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        || line_contains_kind(input, SyntaxKind::Comment);
188    consume_line(input);
189    Ok(ParsedTopLevelItem::Namespace { malformed })
190}
191
192fn parse_task_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
193    token_kind(input, SyntaxKind::Ident)?;
194    let mut saw_colon = false;
195    let mut header_complete = false;
196    let mut line_start = false;
197    let mut malformed = false;
198    let mut expect_guard_at = false;
199    let mut phase = TaskHeaderPhase::BeforeTail;
200
201    while let Some(kind) = input.first().copied() {
202        if header_complete && line_start && starts_top_level_item(kind) {
203            break;
204        }
205
206        if !header_complete {
207            if kind == SyntaxKind::Comment {
208                malformed = true;
209            }
210
211            match &mut phase {
212                TaskHeaderPhase::BeforeTail => match kind {
213                    SyntaxKind::LParen => {
214                        phase = TaskHeaderPhase::Params { depth: 1 };
215                    }
216                    SyntaxKind::Question => {
217                        phase = TaskHeaderPhase::Guard { depth: 0 };
218                        expect_guard_at = true;
219                    }
220                    SyntaxKind::Amp => {
221                        phase = TaskHeaderPhase::Dependencies {
222                            group_depth: 0,
223                            saw_group: false,
224                        };
225                    }
226                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
227                    SyntaxKind::At if expect_guard_at => {
228                        expect_guard_at = false;
229                    }
230                    _ => {
231                        if expect_guard_at {
232                            malformed = true;
233                            expect_guard_at = false;
234                        }
235                    }
236                },
237                TaskHeaderPhase::Params { depth } => match kind {
238                    SyntaxKind::LParen => *depth += 1,
239                    SyntaxKind::RParen => {
240                        if *depth == 0 {
241                            malformed = true;
242                        } else {
243                            *depth -= 1;
244                            if *depth == 0 {
245                                phase = TaskHeaderPhase::BeforeTail;
246                            }
247                        }
248                    }
249                    _ => {}
250                },
251                TaskHeaderPhase::Guard { depth } => match kind {
252                    SyntaxKind::LParen => *depth += 1,
253                    SyntaxKind::RParen => {
254                        if *depth > 0 {
255                            *depth -= 1;
256                        }
257                        if *depth == 0 {
258                            phase = TaskHeaderPhase::BeforeTail;
259                        }
260                    }
261                    SyntaxKind::At if expect_guard_at => {
262                        expect_guard_at = false;
263                    }
264                    SyntaxKind::Whitespace | SyntaxKind::Indent => {}
265                    _ => {
266                        if expect_guard_at {
267                            malformed = true;
268                            expect_guard_at = false;
269                        }
270                    }
271                },
272                TaskHeaderPhase::Dependencies {
273                    group_depth,
274                    saw_group,
275                } => match kind {
276                    SyntaxKind::LParen => {
277                        if *group_depth > 0 {
278                            malformed = true;
279                        }
280                        *group_depth += 1;
281                        *saw_group = true;
282                    }
283                    SyntaxKind::RParen => {
284                        if *group_depth == 0 {
285                            malformed = true;
286                        } else {
287                            *group_depth -= 1;
288                        }
289                    }
290                    SyntaxKind::Question | SyntaxKind::At => malformed = true,
291                    SyntaxKind::ShellKw | SyntaxKind::ShellFallbackKw if *group_depth == 0 => {
292                        phase = TaskHeaderPhase::Shell;
293                    }
294                    SyntaxKind::Unknown if kind == SyntaxKind::Unknown => {}
295                    _ => {}
296                },
297                TaskHeaderPhase::Shell => {}
298            }
299        }
300
301        if kind == SyntaxKind::Colon {
302            saw_colon = true;
303        }
304        advance(input);
305
306        if kind == SyntaxKind::Eof {
307            break;
308        }
309
310        if kind == SyntaxKind::Newline && !saw_colon {
311            malformed |= !phase.is_balanced() || expect_guard_at;
312            break;
313        }
314
315        if kind == SyntaxKind::Newline && saw_colon {
316            malformed |= !phase.is_balanced() || expect_guard_at;
317            header_complete = true;
318        }
319
320        line_start = kind == SyntaxKind::Newline;
321    }
322
323    Ok(ParsedTopLevelItem::Task {
324        saw_colon,
325        malformed,
326    })
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330enum TaskHeaderPhase {
331    BeforeTail,
332    Params { depth: usize },
333    Guard { depth: usize },
334    Dependencies { group_depth: usize, saw_group: bool },
335    Shell,
336}
337
338impl TaskHeaderPhase {
339    fn is_balanced(self) -> bool {
340        match self {
341            TaskHeaderPhase::BeforeTail | TaskHeaderPhase::Shell => true,
342            TaskHeaderPhase::Params { depth } | TaskHeaderPhase::Guard { depth } => depth == 0,
343            TaskHeaderPhase::Dependencies { group_depth, .. } => group_depth == 0,
344        }
345    }
346}
347
348fn parse_unexpected_item(input: &mut &[SyntaxKind]) -> ModalResult<ParsedTopLevelItem> {
349    any::<_, ErrMode<ContextError>>
350        .verify(|kind: &SyntaxKind| !is_trivia(*kind) && *kind != SyntaxKind::Eof)
351        .value(ParsedTopLevelItem::Unexpected)
352        .parse_next(input)
353}
354
355fn token_kind(input: &mut &[SyntaxKind], kind: SyntaxKind) -> ModalResult<SyntaxKind> {
356    any::<_, ErrMode<ContextError>>
357        .verify(move |candidate: &SyntaxKind| *candidate == kind)
358        .parse_next(input)
359}
360
361fn parse_error(code: &str, message: &str, range: TextRange) -> Diagnostic {
362    Diagnostic::new(
363        DiagnosticSeverity::Error,
364        DiagnosticCode::new(code),
365        message,
366        DiagnosticPhase::Parse,
367        normalize_range(range),
368    )
369}
370
371fn normalize_range(range: TextRange) -> TextRange {
372    if range.is_empty() {
373        TextRange::new(range.start(), range.start() + TextSize::from(1))
374    } else {
375        range
376    }
377}