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}