Skip to main content

shuck_parser/parser/
result.rs

1use shuck_ast::{File, Span};
2
3use crate::error::Error;
4
5/// Overall outcome of a parse attempt.
6///
7/// A parse can produce an AST even when recovery diagnostics were needed.
8/// Check [`ParseResult::is_ok`] when a caller requires a fully clean parse.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ParseStatus {
11    /// The parse completed without recovery diagnostics.
12    Clean,
13    /// The parse completed, but required recovery diagnostics.
14    Recovered,
15    /// The parse stopped after a terminal error.
16    Fatal,
17}
18
19/// One branch separator recognized inside a zsh `case` pattern group.
20///
21/// These separators are exposed as syntax facts because they describe source
22/// surface that downstream tools may need even when the AST normalizes the
23/// surrounding pattern structure.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ZshCaseGroupPart {
26    /// Index of the owning pattern part within the parsed pattern.
27    pub pattern_part_index: usize,
28    /// Source span covering the separator syntax.
29    pub span: Span,
30}
31
32/// Additional parser-owned facts that are useful to downstream consumers.
33///
34/// These facts preserve parser discoveries that are awkward to represent
35/// directly in the shared AST but are still relevant to linting, formatting, or
36/// source remapping.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct SyntaxFacts {
39    /// Spans of zsh brace-style `if` bodies.
40    pub zsh_brace_if_spans: Vec<Span>,
41    /// Spans of zsh `always` clauses.
42    pub zsh_always_spans: Vec<Span>,
43    /// Pattern-group separators collected from zsh `case` items.
44    pub zsh_case_group_parts: Vec<ZshCaseGroupPart>,
45}
46
47/// A parser diagnostic emitted while recovering from invalid input.
48///
49/// Diagnostics use Shuck-authored wording and source spans in the parsed input.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ParseDiagnostic {
52    /// Human-readable diagnostic message.
53    pub message: String,
54    /// Source span associated with the diagnostic.
55    pub span: Span,
56}
57
58/// The result of parsing a script.
59///
60/// `ParseResult` always carries the best AST the parser produced. Inspect
61/// [`ParseResult::status`] or [`ParseResult::is_ok`] before assuming it was a
62/// clean parse.
63#[derive(Debug, Clone)]
64pub struct ParseResult {
65    /// Parsed syntax tree for the file.
66    pub file: File,
67    /// Recovery diagnostics emitted while producing the AST.
68    pub diagnostics: Vec<ParseDiagnostic>,
69    /// High-level parse status.
70    pub status: ParseStatus,
71    /// Terminal parse error, when recovery could not continue.
72    pub terminal_error: Option<Error>,
73    /// Additional syntax facts collected during parsing.
74    pub syntax_facts: SyntaxFacts,
75}
76
77impl ParseResult {
78    /// Returns `true` when the parse completed without recovery diagnostics.
79    pub fn is_ok(&self) -> bool {
80        self.status == ParseStatus::Clean
81    }
82
83    /// Returns `true` when the parse produced recovery diagnostics or a terminal error.
84    pub fn is_err(&self) -> bool {
85        !self.is_ok()
86    }
87
88    /// Convert this result into a strict parse error.
89    ///
90    /// If recovery diagnostics exist but no terminal error was recorded, the first recovery
91    /// diagnostic is converted into an [`Error`].
92    pub fn strict_error(&self) -> Error {
93        self.terminal_error.clone().unwrap_or_else(|| {
94            let Some(diagnostic) = self.diagnostics.first() else {
95                panic!("non-clean parse result should include a diagnostic or terminal error");
96            };
97            Error::parse_at(
98                diagnostic.message.clone(),
99                diagnostic.span.start.line,
100                diagnostic.span.start.column,
101            )
102        })
103    }
104
105    /// Return the parse result when it is clean, otherwise panic with the strict error.
106    pub fn unwrap(self) -> Self {
107        if self.is_ok() {
108            self
109        } else {
110            panic!(
111                "called `ParseResult::unwrap()` on a non-clean parse: {}",
112                self.strict_error()
113            )
114        }
115    }
116
117    /// Return the parse result when it is clean, otherwise panic with `message`.
118    pub fn expect(self, message: &str) -> Self {
119        if self.is_ok() {
120            self
121        } else {
122            panic!("{message}: {}", self.strict_error())
123        }
124    }
125
126    /// Return the strict parse error when the result is not clean, otherwise panic.
127    pub fn unwrap_err(self) -> Error {
128        if self.is_err() {
129            self.strict_error()
130        } else {
131            panic!("called `ParseResult::unwrap_err()` on a clean parse")
132        }
133    }
134
135    /// Return the strict parse error when the result is not clean, otherwise panic with
136    /// `message`.
137    pub fn expect_err(self, message: &str) -> Error {
138        if self.is_err() {
139            self.strict_error()
140        } else {
141            panic!("{message}")
142        }
143    }
144}