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 fuel limit.
36    ///
37    /// Fuel bounds the number of parser operations. Exhaustion produces a
38    /// terminal parse error in the returned [`ParseResult`].
39    pub fn with_fuel(input: &'a str, max_fuel: usize) -> Self {
40        Self::with_limits_and_profile(
41            input,
42            DEFAULT_MAX_AST_DEPTH,
43            max_fuel,
44            ShellProfile::native(ShellDialect::Bash),
45        )
46    }
47
48    /// Create a new parser with custom depth, fuel, and shell-profile settings.
49    ///
50    /// This is the most explicit constructor for embedders that need both
51    /// resource limits and parser-visible shell option state.
52    pub fn with_limits_and_profile(
53        input: &'a str,
54        max_depth: usize,
55        max_fuel: usize,
56        shell_profile: ShellProfile,
57    ) -> Self {
58        Self::with_limits_and_profile_and_benchmarking(
59            input,
60            max_depth,
61            max_fuel,
62            shell_profile,
63            false,
64        )
65    }
66
67    pub(super) fn with_limits_and_profile_and_benchmarking(
68        input: &'a str,
69        max_depth: usize,
70        max_fuel: usize,
71        shell_profile: ShellProfile,
72        benchmark_counters_enabled: bool,
73    ) -> Self {
74        #[cfg(not(feature = "benchmarking"))]
75        let _ = benchmark_counters_enabled;
76
77        let zsh_timeline = (shell_profile.dialect == ShellDialect::Zsh)
78            .then(|| ZshOptionTimeline::build(input, &shell_profile))
79            .flatten()
80            .map(Arc::new);
81        let mut lexer = Lexer::with_max_subst_depth_and_profile(
82            input,
83            max_depth.min(HARD_MAX_AST_DEPTH),
84            &shell_profile,
85            zsh_timeline.clone(),
86        );
87        #[cfg(feature = "benchmarking")]
88        if benchmark_counters_enabled {
89            lexer.enable_benchmark_counters();
90        }
91        let mut comments = Vec::new();
92        let (current_token, current_token_kind, current_keyword, current_span) = loop {
93            match lexer.next_lexed_token_with_comments() {
94                Some(st) if st.kind == TokenKind::Comment => {
95                    comments.push(Comment {
96                        range: st.span.to_range(),
97                    });
98                }
99                Some(st) => {
100                    break (
101                        Some(st.clone()),
102                        Some(st.kind),
103                        Self::keyword_from_token(&st),
104                        st.span,
105                    );
106                }
107                None => break (None, None, None, Span::new()),
108            }
109        };
110        Self {
111            input,
112            lexer,
113            synthetic_tokens: VecDeque::new(),
114            alias_replays: Vec::new(),
115            current_token,
116            current_word_cache: None,
117            current_token_kind,
118            current_keyword,
119            current_span,
120            peeked_token: None,
121            max_depth: max_depth.min(HARD_MAX_AST_DEPTH),
122            current_depth: 0,
123            fuel: max_fuel,
124            max_fuel,
125            source_text_pattern_depth: 0,
126            comments,
127            aliases: HashMap::new(),
128            expand_aliases: false,
129            expand_next_word: false,
130            brace_group_depth: 0,
131            brace_body_stack: Vec::new(),
132            syntax_facts: SyntaxFacts::default(),
133            dialect: shell_profile.dialect,
134            shell_profile,
135            zsh_timeline,
136            brace_scan_chars: std::cell::RefCell::new(Vec::new()),
137            #[cfg(feature = "benchmarking")]
138            benchmark_counters: benchmark_counters_enabled.then(ParserBenchmarkCounters::default),
139        }
140    }
141
142    #[cfg(feature = "benchmarking")]
143    pub(super) fn rebuild_with_benchmark_counters(&self) -> Self {
144        Self::with_limits_and_profile_and_benchmarking(
145            self.input,
146            self.max_depth,
147            self.max_fuel,
148            self.shell_profile.clone(),
149            true,
150        )
151    }
152
153    #[cfg(test)]
154    pub(super) fn current_span(&self) -> Span {
155        self.current_span
156    }
157
158    /// Parse a standalone shell word string.
159    ///
160    /// This handles shell word constructs such as parameter expansion, command
161    /// substitution, arithmetic expansion, and quoting. The returned word is
162    /// positioned as if `input` started at the beginning of a file.
163    pub fn parse_word_string(input: &str) -> Word {
164        let mut parser = Parser::new(input);
165        let start = Position::new();
166        parser.parse_word_with_context(
167            input,
168            Span::from_positions(start, start.advanced_by(input)),
169            start,
170            true,
171        )
172    }
173
174    /// Classify a contiguous group of already-parsed words as a shell assignment.
175    ///
176    /// Some shell syntax, such as process substitution inside an array subscript,
177    /// can produce multiple AST words while still occupying one contiguous
178    /// assignment operand in the source.
179    pub fn parse_assignment_word_group(
180        source: &str,
181        words: &[&Word],
182        explicit_array_kind: Option<ArrayKind>,
183        subscript_interpretation: SubscriptInterpretation,
184    ) -> Option<Assignment> {
185        let first = words.first()?;
186        let last = words.last()?;
187        let span = Span::from_positions(first.span.start, last.span.end);
188        let raw = span.slice(source);
189        let mut parser = Parser::new(source);
190        parser.parse_assignment_from_text(raw, span, explicit_array_kind, subscript_interpretation)
191    }
192
193    /// Parse a word string with caller-configured limits and shell dialect.
194    pub(super) fn parse_word_string_with_limits_and_dialect(
195        input: &str,
196        max_depth: usize,
197        max_fuel: usize,
198        dialect: ShellDialect,
199    ) -> Word {
200        let mut parser = Parser::with_limits_and_profile(
201            input,
202            max_depth,
203            max_fuel,
204            ShellProfile::native(dialect),
205        );
206        let start = Position::new();
207        parser.parse_word_with_context(
208            input,
209            Span::from_positions(start, start.advanced_by(input)),
210            start,
211            true,
212        )
213    }
214
215    /// Parse a fragment against the original source span so part offsets stay
216    /// aligned with the surrounding script.
217    #[cfg(test)]
218    pub(super) fn parse_word_fragment(source: &str, text: &str, span: Span) -> Word {
219        Self::parse_word_fragment_with_limits(
220            source,
221            text,
222            span,
223            DEFAULT_MAX_AST_DEPTH,
224            DEFAULT_MAX_PARSER_OPERATIONS,
225            ShellProfile::native(ShellDialect::Bash),
226        )
227    }
228
229    pub(super) fn parse_word_fragment_with_limits(
230        source: &str,
231        text: &str,
232        span: Span,
233        max_depth: usize,
234        max_fuel: usize,
235        shell_profile: ShellProfile,
236    ) -> Word {
237        let mut parser = Parser::with_limits_and_profile(text, max_depth, max_fuel, shell_profile);
238        let source_backed = span.end.offset() <= source.len() && span.slice(source) == text;
239        let start = Position::new();
240        let fragment_span = Span::from_positions(start, start.advanced_by(text));
241        let mut word = parser.parse_word_with_context(text, fragment_span, start, source_backed);
242        if !source_backed {
243            Self::materialize_word_source_backing(&mut word, text);
244        }
245        Self::rebase_word(&mut word, span.start);
246        word.span = span;
247        word
248    }
249}