Skip to main content

shuck_parser/parser/
entry.rs

1use super::*;
2
3impl<'a> Parser<'a> {
4    /// Create a new bash-profile parser for the given input.
5    pub fn new(input: &'a str) -> Self {
6        Self::with_limits_and_profile(
7            input,
8            DEFAULT_MAX_AST_DEPTH,
9            DEFAULT_MAX_PARSER_OPERATIONS,
10            ShellProfile::native(ShellDialect::Bash),
11        )
12    }
13
14    /// Create a new parser for the given input and shell dialect.
15    ///
16    /// This uses [`ShellProfile::native`] for the selected dialect. Use
17    /// [`Parser::with_profile`] when zsh option state is known.
18    pub fn with_dialect(input: &'a str, dialect: ShellDialect) -> Self {
19        Self::with_profile(input, ShellProfile::native(dialect))
20    }
21
22    /// Create a new parser for the given input and full shell profile.
23    ///
24    /// Profiles allow callers to provide parser-visible zsh option state in
25    /// addition to the broad shell dialect.
26    pub fn with_profile(input: &'a str, shell_profile: ShellProfile) -> Self {
27        Self::with_limits_and_profile(
28            input,
29            DEFAULT_MAX_AST_DEPTH,
30            DEFAULT_MAX_PARSER_OPERATIONS,
31            shell_profile,
32        )
33    }
34
35    /// Create a new bash parser with a custom maximum AST depth.
36    ///
37    /// The requested depth is clamped to the parser's hard safety cap. Hitting
38    /// the limit produces a non-clean [`ParseResult`] rather than panicking.
39    pub fn with_max_depth(input: &'a str, max_depth: usize) -> Self {
40        Self::with_limits_and_profile(
41            input,
42            max_depth,
43            DEFAULT_MAX_PARSER_OPERATIONS,
44            ShellProfile::native(ShellDialect::Bash),
45        )
46    }
47
48    /// Create a new bash parser with a custom fuel limit.
49    ///
50    /// Fuel bounds the number of parser operations. Exhaustion produces a
51    /// terminal parse error in the returned [`ParseResult`].
52    pub fn with_fuel(input: &'a str, max_fuel: usize) -> Self {
53        Self::with_limits_and_profile(
54            input,
55            DEFAULT_MAX_AST_DEPTH,
56            max_fuel,
57            ShellProfile::native(ShellDialect::Bash),
58        )
59    }
60
61    /// Create a new bash parser with custom depth and fuel limits.
62    ///
63    /// `max_depth` is clamped to the parser's hard safety cap to prevent stack
64    /// overflow from misconfiguration. `max_fuel` bounds parser operations.
65    /// Either limit can produce a non-clean [`ParseResult`].
66    pub fn with_limits(input: &'a str, max_depth: usize, max_fuel: usize) -> Self {
67        Self::with_limits_and_profile(
68            input,
69            max_depth,
70            max_fuel,
71            ShellProfile::native(ShellDialect::Bash),
72        )
73    }
74
75    /// Create a new parser with custom depth, fuel, and dialect settings.
76    ///
77    /// This uses [`ShellProfile::native`] for `dialect`; use
78    /// [`Parser::with_limits_and_profile`] when explicit zsh option state is
79    /// available.
80    pub fn with_limits_and_dialect(
81        input: &'a str,
82        max_depth: usize,
83        max_fuel: usize,
84        dialect: ShellDialect,
85    ) -> Self {
86        Self::with_limits_and_profile(input, max_depth, max_fuel, ShellProfile::native(dialect))
87    }
88
89    /// Create a new parser with custom depth, fuel, and shell-profile settings.
90    ///
91    /// This is the most explicit constructor for embedders that need both
92    /// resource limits and parser-visible shell option state.
93    pub fn with_limits_and_profile(
94        input: &'a str,
95        max_depth: usize,
96        max_fuel: usize,
97        shell_profile: ShellProfile,
98    ) -> Self {
99        Self::with_limits_and_profile_and_benchmarking(
100            input,
101            max_depth,
102            max_fuel,
103            shell_profile,
104            false,
105        )
106    }
107
108    pub(super) fn with_limits_and_profile_and_benchmarking(
109        input: &'a str,
110        max_depth: usize,
111        max_fuel: usize,
112        shell_profile: ShellProfile,
113        benchmark_counters_enabled: bool,
114    ) -> Self {
115        #[cfg(not(feature = "benchmarking"))]
116        let _ = benchmark_counters_enabled;
117
118        let zsh_timeline = (shell_profile.dialect == ShellDialect::Zsh)
119            .then(|| ZshOptionTimeline::build(input, &shell_profile))
120            .flatten()
121            .map(Arc::new);
122        let mut lexer = Lexer::with_max_subst_depth_and_profile(
123            input,
124            max_depth.min(HARD_MAX_AST_DEPTH),
125            &shell_profile,
126            zsh_timeline.clone(),
127        );
128        #[cfg(feature = "benchmarking")]
129        if benchmark_counters_enabled {
130            lexer.enable_benchmark_counters();
131        }
132        let mut comments = Vec::new();
133        let (current_token, current_token_kind, current_keyword, current_span) = loop {
134            match lexer.next_lexed_token_with_comments() {
135                Some(st) if st.kind == TokenKind::Comment => {
136                    comments.push(Comment {
137                        range: st.span.to_range(),
138                    });
139                }
140                Some(st) => {
141                    break (
142                        Some(st.clone()),
143                        Some(st.kind),
144                        Self::keyword_from_token(&st),
145                        st.span,
146                    );
147                }
148                None => break (None, None, None, Span::new()),
149            }
150        };
151        Self {
152            input,
153            lexer,
154            synthetic_tokens: VecDeque::new(),
155            alias_replays: Vec::new(),
156            current_token,
157            current_word_cache: None,
158            current_token_kind,
159            current_keyword,
160            current_span,
161            peeked_token: None,
162            max_depth: max_depth.min(HARD_MAX_AST_DEPTH),
163            current_depth: 0,
164            fuel: max_fuel,
165            max_fuel,
166            source_text_pattern_depth: 0,
167            comments,
168            aliases: HashMap::new(),
169            expand_aliases: false,
170            expand_next_word: false,
171            brace_group_depth: 0,
172            brace_body_stack: Vec::new(),
173            syntax_facts: SyntaxFacts::default(),
174            dialect: shell_profile.dialect,
175            shell_profile,
176            zsh_timeline,
177            #[cfg(feature = "benchmarking")]
178            benchmark_counters: benchmark_counters_enabled.then(ParserBenchmarkCounters::default),
179        }
180    }
181
182    #[cfg(feature = "benchmarking")]
183    pub(super) fn rebuild_with_benchmark_counters(&self) -> Self {
184        Self::with_limits_and_profile_and_benchmarking(
185            self.input,
186            self.max_depth,
187            self.max_fuel,
188            self.shell_profile.clone(),
189            true,
190        )
191    }
192
193    #[cfg(test)]
194    pub(super) fn current_span(&self) -> Span {
195        self.current_span
196    }
197
198    /// Parse a standalone shell word string.
199    ///
200    /// This handles shell word constructs such as parameter expansion, command
201    /// substitution, arithmetic expansion, and quoting. The returned word is
202    /// positioned as if `input` started at the beginning of a file.
203    pub fn parse_word_string(input: &str) -> Word {
204        let mut parser = Parser::new(input);
205        let start = Position::new();
206        parser.parse_word_with_context(
207            input,
208            Span::from_positions(start, start.advanced_by(input)),
209            start,
210            true,
211        )
212    }
213
214    /// Classify a contiguous group of already-parsed words as a shell assignment.
215    ///
216    /// Some shell syntax, such as process substitution inside an array subscript,
217    /// can produce multiple AST words while still occupying one contiguous
218    /// assignment operand in the source.
219    pub fn parse_assignment_word_group(
220        source: &str,
221        words: &[&Word],
222        explicit_array_kind: Option<ArrayKind>,
223        subscript_interpretation: SubscriptInterpretation,
224    ) -> Option<Assignment> {
225        let first = words.first()?;
226        let last = words.last()?;
227        let span = Span::from_positions(first.span.start, last.span.end);
228        let raw = span.slice(source);
229        let mut parser = Parser::new(source);
230        parser.parse_assignment_from_text(raw, span, explicit_array_kind, subscript_interpretation)
231    }
232
233    /// Parse a word string with caller-configured limits and shell dialect.
234    pub(super) fn parse_word_string_with_limits_and_dialect(
235        input: &str,
236        max_depth: usize,
237        max_fuel: usize,
238        dialect: ShellDialect,
239    ) -> Word {
240        let mut parser = Parser::with_limits_and_profile(
241            input,
242            max_depth,
243            max_fuel,
244            ShellProfile::native(dialect),
245        );
246        let start = Position::new();
247        parser.parse_word_with_context(
248            input,
249            Span::from_positions(start, start.advanced_by(input)),
250            start,
251            true,
252        )
253    }
254
255    /// Parse a fragment against the original source span so part offsets stay
256    /// aligned with the surrounding script.
257    #[cfg(test)]
258    pub(super) fn parse_word_fragment(source: &str, text: &str, span: Span) -> Word {
259        Self::parse_word_fragment_with_limits(
260            source,
261            text,
262            span,
263            DEFAULT_MAX_AST_DEPTH,
264            DEFAULT_MAX_PARSER_OPERATIONS,
265            ShellProfile::native(ShellDialect::Bash),
266        )
267    }
268
269    pub(super) fn parse_word_fragment_with_limits(
270        source: &str,
271        text: &str,
272        span: Span,
273        max_depth: usize,
274        max_fuel: usize,
275        shell_profile: ShellProfile,
276    ) -> Word {
277        let mut parser = Parser::with_limits_and_profile(text, max_depth, max_fuel, shell_profile);
278        let source_backed = span.end.offset <= source.len() && span.slice(source) == text;
279        let start = Position::new();
280        let fragment_span = Span::from_positions(start, start.advanced_by(text));
281        let mut word = parser.parse_word_with_context(text, fragment_span, start, source_backed);
282        if !source_backed {
283            Self::materialize_word_source_backing(&mut word, text);
284        }
285        Self::rebase_word(&mut word, span.start);
286        word.span = span;
287        word
288    }
289}