Skip to main content

brush_core/
callstack.rs

1//! Call stack representations.
2
3use crate::{functions, traps};
4use std::{
5    borrow::Cow,
6    collections::{HashSet, VecDeque},
7    sync::Arc,
8};
9
10use brush_parser::ast::SourceLocation;
11
12/// Encapsulates info regarding a script call.
13#[derive(Clone, Debug)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct ScriptCall {
16    /// The type of script call.
17    pub call_type: ScriptCallType,
18    /// The source info for the script called.
19    pub source_info: crate::SourceInfo,
20}
21
22impl ScriptCall {
23    /// Returns the name of the script that was called.
24    pub fn name(&self) -> Cow<'_, str> {
25        self.source_info.source.as_str().into()
26    }
27}
28
29/// The type of script call.
30#[derive(Clone, Copy, Debug)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub enum ScriptCallType {
33    /// A script was sourced.
34    Source,
35    /// A script was executed.
36    Run,
37}
38
39impl std::fmt::Display for ScriptCall {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self.call_type {
42            ScriptCallType::Source => write!(f, "source({})", self.source_info),
43            ScriptCallType::Run => write!(f, "script({})", self.source_info),
44        }
45    }
46}
47
48/// Represents the type of a frame, indicating how it was invoked from
49/// a different source context.
50#[derive(Clone, Debug)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub enum FrameType {
53    /// A script was called (sourced or executed).
54    Script(ScriptCall),
55    /// A function was called.
56    Function(FunctionCall),
57    /// A trap handler was invoked.
58    TrapHandler(traps::TrapSignal),
59    /// A string was eval'd.
60    Eval,
61    /// A command-line string (i.e., -c) was executed.
62    CommandString,
63    /// An interactive command session was started.
64    InteractiveSession,
65}
66
67impl FrameType {
68    /// Returns a name for the frame (i.e., script path or function name).
69    pub fn name(&self) -> Cow<'_, str> {
70        match self {
71            Self::Script(call) => call.name(),
72            Self::Function(call) => call.name(),
73            Self::TrapHandler(_) => "trap".into(),
74            Self::Eval => "eval".into(),
75            Self::CommandString => "-c".into(),
76            Self::InteractiveSession => "interactive".into(),
77        }
78    }
79
80    /// Returns `true` if the frame is for a function call.
81    pub const fn is_function(&self) -> bool {
82        matches!(self, Self::Function(..))
83    }
84
85    /// Returns `true` if the frame is for a script call.
86    pub const fn is_script(&self) -> bool {
87        matches!(self, Self::Script(..))
88    }
89
90    /// Returns `true` if the frame is for a trap handler.
91    pub const fn is_trap_handler(&self) -> bool {
92        matches!(self, Self::TrapHandler(_))
93    }
94
95    /// Returns `true` if the frame is for an interactive session.
96    pub const fn is_interactive_session(&self) -> bool {
97        matches!(self, Self::InteractiveSession)
98    }
99
100    /// Returns `true` if the frame is for a command string being executed.
101    pub const fn is_command_string(&self) -> bool {
102        matches!(self, Self::CommandString)
103    }
104
105    /// Returns `true` if the frame is for a sourced script.
106    pub const fn is_sourced_script(&self) -> bool {
107        matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Source))
108    }
109
110    /// Returns `true` if the frame is for a run script.
111    pub const fn is_run_script(&self) -> bool {
112        matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Run))
113    }
114}
115
116impl std::fmt::Display for FrameType {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        match self {
119            Self::Script(call) => call.fmt(f),
120            Self::Function(call) => call.fmt(f),
121            Self::TrapHandler(_) => write!(f, "trap"),
122            Self::Eval => write!(f, "eval"),
123            Self::CommandString => write!(f, "-c"),
124            Self::InteractiveSession => write!(f, "interactive"),
125        }
126    }
127}
128
129/// Describes the target of a function call.
130#[derive(Clone, Debug)]
131#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
132pub struct FunctionCall {
133    /// The name of the function invoked.
134    pub function_name: String,
135    /// The invoked function.
136    pub function: functions::Registration,
137}
138
139impl FunctionCall {
140    /// Returns the name of the function that was called.
141    pub fn name(&self) -> Cow<'_, str> {
142        self.function_name.as_str().into()
143    }
144}
145
146impl std::fmt::Display for FunctionCall {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        write!(f, "func({})", self.function_name)
149    }
150}
151
152/// Represents a single frame in a `CallStack`.
153#[derive(Clone, Debug)]
154#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
155pub struct Frame {
156    /// The type of frame.
157    pub frame_type: FrameType,
158    /// The source information for the frame. The locations associated with AST nodes
159    /// executed in this frame should be interpreted as being relative to this
160    /// source info.
161    pub source_info: crate::SourceInfo,
162    /// The location of the entry point into this frame, within the frame of
163    /// reference of `source_info`. May be `None` if the entry point is not known.
164    pub entry: Option<Arc<crate::SourcePosition>>,
165    /// Information about the currently executing location. For the topmost frame on
166    /// the stack, this represents the current execution location. For older frames,
167    /// this represents the site from which a control transfer was made to the next
168    /// younger frame. May be `None` if the current location is not known. When present,
169    /// it is relative to the frame of reference of `source_info`.
170    pub current: Option<Arc<crate::SourcePosition>>,
171    /// Positional arguments (not including $0). May not be present for all frames.
172    pub args: Vec<String>,
173    /// Optionally, indicates an additional line offset within the current source context.
174    pub current_line_offset: usize,
175}
176
177impl Frame {
178    /// Returns the adjusted source info for this frame, combining the
179    /// frame's `source_info` and `current_line_offset`, if present.
180    pub fn adjusted_source_info(&self) -> crate::SourceInfo {
181        self.pos_as_source_info(None)
182    }
183
184    /// Returns the current position as a new `SourceInfo`, combining the
185    /// frame's `source_info` and `current` position.
186    pub fn current_pos_as_source_info(&self) -> crate::SourceInfo {
187        self.pos_as_source_info(self.current.as_ref())
188    }
189
190    fn pos_as_source_info(&self, pos: Option<&Arc<crate::SourcePosition>>) -> crate::SourceInfo {
191        let mut new_start = if let Some(existing_start) = &self.source_info.start {
192            if let Some(current) = pos {
193                Some(Arc::new(crate::SourcePosition {
194                    index: existing_start.index + current.index,
195                    line: existing_start.line + (current.line - 1),
196                    column: if current.line <= 1 {
197                        existing_start.column + (current.column - 1)
198                    } else {
199                        current.column
200                    },
201                }))
202            } else {
203                Some(existing_start.clone())
204            }
205        } else {
206            pos.cloned()
207        };
208
209        if self.current_line_offset > 0 {
210            new_start = if let Some(new_start) = new_start {
211                let mut pos = (*new_start).clone();
212                pos.line += self.current_line_offset;
213
214                Some(Arc::new(pos))
215            } else {
216                Some(Arc::new(crate::SourcePosition {
217                    index: 0,
218                    line: self.current_line_offset + 1,
219                    column: 1,
220                }))
221            };
222        }
223
224        crate::SourceInfo {
225            source: self.source_info.source.clone(),
226            start: new_start,
227        }
228    }
229
230    /// Returns the current line number.
231    pub fn current_line(&self) -> Option<usize> {
232        let start_line = self.source_info.start.as_ref().map_or(1, |pos| pos.line);
233        let current_line = self.current.as_ref().map(|pos| pos.line)?;
234
235        Some(start_line.saturating_sub(1) + current_line + self.current_line_offset)
236    }
237
238    /// Returns the current line number, relative to the frame's entry.
239    pub fn current_frame_relative_line(&self) -> Option<usize> {
240        let current_line = self.current.as_ref().map(|pos| pos.line)?;
241        let entry_line = self.entry.as_ref().map_or(1, |pos| pos.line);
242
243        Some(current_line.saturating_sub(entry_line) + self.current_line_offset + 1)
244    }
245}
246
247/// Options for formatting a call stack.
248#[derive(Default)]
249pub struct FormatOptions {
250    /// Whether or not to show args.
251    pub show_args: bool,
252    /// Whether or not to show frame entry points.
253    pub show_entry_points: bool,
254}
255
256/// Helper struct for formatting a call stack with custom options.
257///
258/// This struct implements `Display` and can be used to write a formatted
259/// call stack to any type that implements `io::Write`.
260pub struct FormatCallStack<'a> {
261    stack: &'a CallStack,
262    options: &'a FormatOptions,
263}
264
265impl<'a> FormatCallStack<'a> {
266    /// Creates a new formatter for the given call stack with the specified options.
267    ///
268    /// # Arguments
269    ///
270    /// * `stack` - The call stack to format.
271    /// * `options` - The formatting options to use.
272    pub const fn new(stack: &'a CallStack, options: &'a FormatOptions) -> Self {
273        Self { stack, options }
274    }
275}
276
277impl std::fmt::Display for FormatCallStack<'_> {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        self.stack.fmt_with_options(f, self.options)
280    }
281}
282
283/// Encapsulates a script call stack.
284#[derive(Clone, Debug, Default)]
285#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
286pub struct CallStack {
287    frames: VecDeque<Frame>,
288    func_call_depth: usize,
289    script_source_depth: usize,
290    active_trap_signals: HashSet<traps::TrapSignal>,
291    trap_delivery_suppress_count: usize,
292}
293
294impl CallStack {
295    /// Creates a formatter for this call stack with the given options.
296    ///
297    /// # Arguments
298    ///
299    /// * `options` - The formatting options to use.
300    pub const fn format<'a>(&'a self, options: &'a FormatOptions) -> FormatCallStack<'a> {
301        FormatCallStack::new(self, options)
302    }
303
304    /// Formats the call stack with the given options.
305    ///
306    /// # Arguments
307    ///
308    /// * `f` - The formatter to write to.
309    /// * `options` - The formatting options.
310    fn fmt_with_options(
311        &self,
312        f: &mut std::fmt::Formatter<'_>,
313        options: &FormatOptions,
314    ) -> std::fmt::Result {
315        if self.is_empty() {
316            return Ok(());
317        }
318
319        color_print::cwriteln!(f, "<underline>Call stack (most recent first):</underline>")?;
320
321        for (index, frame) in self.iter().enumerate() {
322            let si = frame.current_pos_as_source_info();
323
324            color_print::cwrite!(
325                f,
326                "   <dim>#{index}</dim><yellow>|</yellow> <strong>{}</strong>",
327                si.source
328            )?;
329
330            if let Some(pos) = &si.start {
331                color_print::cwrite!(f, ":<cyan>{}</cyan>,<cyan>{}</cyan>", pos.line, pos.column)?;
332            }
333
334            color_print::cwrite!(f, " (<dim>{}</dim>", frame.frame_type)?;
335
336            if options.show_entry_points {
337                if let Some(entry) = &frame.entry {
338                    let entry_si = frame.pos_as_source_info(Some(entry));
339                    if let Some(entry_start) = &entry_si.start {
340                        color_print::cwrite!(
341                            f,
342                            " <dim>entered at {}:{}</dim>",
343                            entry_si.source,
344                            entry_start
345                        )?;
346                    }
347                }
348            }
349
350            color_print::cwriteln!(f, ")")?;
351
352            if !frame.args.is_empty() && options.show_args {
353                for (i, arg) in frame.args.iter().enumerate() {
354                    color_print::cwriteln!(
355                        f,
356                        "     <yellow>${}</yellow>: <blue>{}</blue>",
357                        i + 1,
358                        arg
359                    )?;
360                }
361            }
362        }
363
364        Ok(())
365    }
366}
367
368impl std::fmt::Display for CallStack {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        self.fmt_with_options(f, &FormatOptions::default())
371    }
372}
373
374impl std::ops::Index<usize> for CallStack {
375    type Output = Frame;
376
377    fn index(&self, index: usize) -> &Self::Output {
378        &self.frames[index]
379    }
380}
381
382impl CallStack {
383    /// Creates a new empty script call stack.
384    pub fn new() -> Self {
385        Self::default()
386    }
387
388    /// Removes the top from from the stack. If the stack is empty, does nothing and
389    /// returns `None`; otherwise, returns the removed call frame.
390    pub fn pop(&mut self) -> Option<Frame> {
391        let frame = self.frames.pop_front()?;
392
393        if frame.frame_type.is_function() {
394            self.func_call_depth = self.func_call_depth.saturating_sub(1);
395        }
396
397        if frame.frame_type.is_sourced_script() {
398            self.script_source_depth = self.script_source_depth.saturating_sub(1);
399        }
400
401        if let FrameType::TrapHandler(signal) = &frame.frame_type {
402            self.active_trap_signals.remove(signal);
403        }
404
405        Some(frame)
406    }
407
408    /// Returns a reference to the current (topmost) call frame in the stack.
409    /// Returns `None` if the stack is empty.
410    pub fn current_frame(&self) -> Option<&Frame> {
411        self.frames.front()
412    }
413
414    /// Returns the position in the current (topmost) call frame in the stack,
415    /// expressed as a new `SourceInfo`. Note that this may not be identical
416    /// to that frame's `SourceInfo` since it may include an offset representing
417    /// the current execution position within that source.
418    pub fn current_pos_as_source_info(&self) -> crate::SourceInfo {
419        let Some(frame) = self.frames.front() else {
420            return crate::SourceInfo::default();
421        };
422
423        frame.current_pos_as_source_info()
424    }
425
426    /// Updates the currently executing position in the top stack frame.
427    pub fn set_current_pos(&mut self, position: Option<Arc<crate::SourcePosition>>) {
428        if let Some(frame) = self.frames.front_mut() {
429            frame.current = position;
430        }
431    }
432
433    /// Increments the current line offset in the top stack frame by the given delta.
434    ///
435    /// # Arguments
436    ///
437    /// * `delta` - The number of lines to increment the current line offset by.
438    pub(crate) fn increment_current_line_offset(&mut self, delta: usize) {
439        let Some(frame) = self.frames.front_mut() else {
440            return;
441        };
442
443        frame.current_line_offset += delta;
444    }
445
446    /// Pushes a new script call frame onto the stack.
447    ///
448    /// # Arguments
449    ///
450    /// * `call_type` - The type of script call (sourced or executed).
451    /// * `source_info` - The source of the script.
452    /// * `args` - The positional arguments for the script call.
453    pub fn push_script(
454        &mut self,
455        call_type: ScriptCallType,
456        source_info: &crate::SourceInfo,
457        args: impl IntoIterator<Item = String>,
458    ) {
459        self.frames.push_front(Frame {
460            frame_type: FrameType::Script(ScriptCall {
461                call_type,
462                source_info: source_info.to_owned(),
463            }),
464            args: args.into_iter().collect(),
465            source_info: source_info.to_owned(),
466            current_line_offset: 0,
467            current: None, // TODO(source-info): fill this out
468            entry: None,   // TODO(source-info): fill this out
469        });
470
471        if matches!(call_type, ScriptCallType::Source) {
472            self.script_source_depth += 1;
473        }
474    }
475
476    /// Pushes a new trap handler frame onto the stack.
477    ///
478    /// # Arguments
479    ///
480    /// * `signal` - The signal being handled.
481    /// * `handler` - The trap handler being invoked, if any.
482    pub fn push_trap_handler(
483        &mut self,
484        signal: traps::TrapSignal,
485        handler: Option<&traps::TrapHandler>,
486    ) {
487        let source_info =
488            handler.map_or_else(crate::SourceInfo::default, |h| h.source_info.clone());
489
490        self.frames.push_front(Frame {
491            frame_type: FrameType::TrapHandler(signal),
492            args: vec![],
493            source_info,
494            current_line_offset: 0,
495            current: None, // TODO(source-info): fill this out
496            entry: None,   // TODO(source-info): fill this out
497        });
498
499        self.active_trap_signals.insert(signal);
500    }
501
502    /// Pushes a new eval frame onto the stack.
503    pub fn push_eval(&mut self) {
504        self.frames.push_front(Frame {
505            frame_type: FrameType::Eval,
506            args: vec![],
507            source_info: crate::SourceInfo::from("eval"), // TODO(source-info): fill this out
508            current_line_offset: 0,
509            current: None, // TODO(source-info): fill this out
510            entry: None,   // TODO(source-info): fill this out
511        });
512    }
513
514    /// Pushes a new command string frame onto the stack.
515    pub fn push_command_string(&mut self) {
516        self.frames.push_front(Frame {
517            frame_type: FrameType::CommandString,
518            args: vec![],
519            source_info: crate::SourceInfo::from("environment"),
520            current_line_offset: 0,
521            current: None, // TODO(source-info): fill this out
522            entry: None,   // TODO(source-info): fill this out
523        });
524    }
525
526    /// Pushes a new interactive session frame onto the stack.
527    pub fn push_interactive_session(&mut self) {
528        self.frames.push_front(Frame {
529            frame_type: FrameType::InteractiveSession,
530            args: vec![],
531            current_line_offset: 0,
532            source_info: crate::SourceInfo::from("main"),
533            current: None, // TODO(source-info): fill this out
534            entry: None,   // TODO(source-info): fill this out
535        });
536    }
537
538    /// Pushes a new function call frame onto the stack.
539    ///
540    /// # Arguments
541    ///
542    /// * `name` - The name of the function being called.
543    /// * `function` - The function being called.
544    /// * `args` - The positional arguments for the function call.
545    pub fn push_function(
546        &mut self,
547        name: impl Into<String>,
548        function: &functions::Registration,
549        args: impl IntoIterator<Item = String>,
550    ) {
551        self.frames.push_front(Frame {
552            frame_type: FrameType::Function(FunctionCall {
553                function_name: name.into(),
554                function: function.to_owned(),
555            }),
556            args: args.into_iter().collect(),
557            source_info: function.source().clone(),
558            entry: function.definition().location().map(|span| span.start),
559            current: None, // TODO(source-info): fill this out
560            current_line_offset: 0,
561        });
562
563        self.func_call_depth += 1;
564    }
565
566    /// Iterates through the function calls on the stack.
567    pub fn iter_function_calls(&self) -> impl Iterator<Item = &FunctionCall> {
568        self.iter().filter_map(|frame| {
569            if let FrameType::Function(call) = &frame.frame_type {
570                Some(call)
571            } else {
572                None
573            }
574        })
575    }
576
577    /// Iterates through the script calls on the stack.
578    pub fn iter_script_calls(&self) -> impl Iterator<Item = &ScriptCall> {
579        self.iter().filter_map(|frame| {
580            if let FrameType::Script(call) = &frame.frame_type {
581                Some(call)
582            } else {
583                None
584            }
585        })
586    }
587
588    /// Returns whether or not the current script stack frame is a sourced script.
589    pub fn in_sourced_script(&self) -> bool {
590        self.iter_script_calls()
591            .next()
592            .is_some_and(|call| matches!(call.call_type, ScriptCallType::Source))
593    }
594
595    /// Returns the current depth of function calls in the call stack.
596    pub const fn function_call_depth(&self) -> usize {
597        self.func_call_depth
598    }
599
600    /// Returns the current depth of sourced script calls in the call stack.
601    pub const fn script_source_depth(&self) -> usize {
602        self.script_source_depth
603    }
604
605    /// Returns whether the given trap signal is currently being handled
606    /// (i.e., there is an active frame on the stack for this signal).
607    pub fn is_trap_signal_active(&self, signal: traps::TrapSignal) -> bool {
608        self.active_trap_signals.contains(&signal)
609    }
610
611    /// Clears the set of active trap signals. This should be called when
612    /// creating subshells so they start with fresh trap execution state
613    /// independent of the parent shell's currently-executing traps.
614    pub fn clear_active_trap_signals(&mut self) {
615        self.active_trap_signals.clear();
616    }
617
618    /// Returns whether the given trap signal is currently suppressed.
619    pub const fn is_trap_delivery_suppressed(&self) -> bool {
620        self.trap_delivery_suppress_count > 0
621    }
622
623    /// Acquires a block on trap delivery, preventing traps from being delivered until
624    /// the block is released. Multiple blocks may be acquired, and trap delivery will
625    /// remain suppressed until all blocks have been released.
626    pub const fn acquire_trap_delivery_block(&mut self) {
627        self.trap_delivery_suppress_count += 1;
628    }
629
630    /// Releases a block on trap delivery; note that trap delivery will remain
631    /// suppressed until all blocks have been released.
632    pub const fn release_trap_delivery_block(&mut self) {
633        self.trap_delivery_suppress_count = self.trap_delivery_suppress_count.saturating_sub(1);
634    }
635
636    /// Returns whether or not the shell is actively executing in a shell function.
637    pub fn in_function(&self) -> bool {
638        self.iter_function_calls().next().is_some()
639    }
640
641    /// Returns the current depth of the call stack.
642    pub fn depth(&self) -> usize {
643        self.frames.len()
644    }
645
646    /// Returns whether or not the call stack is empty.
647    pub fn is_empty(&self) -> bool {
648        self.frames.is_empty()
649    }
650
651    /// Returns an iterator over the call frames, starting from the most
652    /// recent.
653    pub fn iter(&self) -> impl Iterator<Item = &Frame> {
654        self.frames.iter()
655    }
656
657    /// Returns a mutable iterator over the call frames, starting from the most
658    /// recent.
659    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Frame> {
660        self.frames.iter_mut()
661    }
662}
663
664#[cfg(test)]
665mod tests {
666    use std::path::PathBuf;
667
668    use super::*;
669    use crate::SourceInfo;
670    use pretty_assertions::assert_matches;
671
672    #[test]
673    fn test_call_stack_new() {
674        let stack = CallStack::new();
675        assert!(stack.is_empty());
676        assert_eq!(stack.depth(), 0);
677    }
678
679    #[test]
680    fn test_call_stack_default() {
681        let stack = CallStack::default();
682        assert!(stack.is_empty());
683        assert_eq!(stack.depth(), 0);
684    }
685
686    #[test]
687    fn test_call_stack_push_pop() {
688        let mut stack = CallStack::new();
689
690        stack.push_script(
691            ScriptCallType::Source,
692            &SourceInfo::from(PathBuf::from("script1.sh")),
693            vec![],
694        );
695        assert!(!stack.is_empty());
696        assert_eq!(stack.depth(), 1);
697
698        stack.push_script(
699            ScriptCallType::Run,
700            &SourceInfo::from(PathBuf::from("script2.sh")),
701            vec![],
702        );
703        assert_eq!(stack.depth(), 2);
704
705        let frame = stack.pop().unwrap();
706        assert_matches!(
707            frame.frame_type,
708            FrameType::Script(ScriptCall {
709                call_type: ScriptCallType::Run,
710                source_info: SourceInfo {
711                    source: file_path,
712                    ..
713                },
714            }) if &file_path == "script2.sh"
715        );
716        assert_eq!(stack.depth(), 1);
717
718        let frame = stack.pop().unwrap();
719        assert_matches!(
720            frame.frame_type,
721            FrameType::Script(ScriptCall {
722                call_type: ScriptCallType::Source,
723                source_info: SourceInfo {
724                    source: file_path,
725                    ..
726                },
727            }) if &file_path == "script1.sh"
728        );
729        assert_eq!(stack.depth(), 0);
730        assert!(stack.is_empty());
731    }
732
733    #[test]
734    fn test_call_stack_pop_empty() {
735        let mut stack = CallStack::new();
736        assert!(stack.pop().is_none());
737    }
738
739    #[test]
740    fn test_in_sourced_script() {
741        let mut stack = CallStack::new();
742        assert!(!stack.in_sourced_script());
743
744        stack.push_script(
745            ScriptCallType::Run,
746            &SourceInfo::from(PathBuf::from("script1.sh")),
747            vec![],
748        );
749        assert!(!stack.in_sourced_script());
750
751        stack.push_script(
752            ScriptCallType::Source,
753            &SourceInfo::from(PathBuf::from("script2.sh")),
754            vec![],
755        );
756        assert!(stack.in_sourced_script());
757
758        stack.pop();
759        assert!(!stack.in_sourced_script());
760    }
761
762    #[test]
763    fn test_call_stack_iter() {
764        let mut stack = CallStack::new();
765        stack.push_script(
766            ScriptCallType::Source,
767            &SourceInfo::from(PathBuf::from("script1.sh")),
768            vec![],
769        );
770        stack.push_script(
771            ScriptCallType::Run,
772            &SourceInfo::from(PathBuf::from("script2.sh")),
773            vec![],
774        );
775        stack.push_script(
776            ScriptCallType::Source,
777            &SourceInfo::from(PathBuf::from("script3.sh")),
778            vec![],
779        );
780
781        let frames: Vec<_> = stack.iter().collect();
782        assert_eq!(frames.len(), 3);
783        assert_matches!(&frames[0].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script3.sh");
784        assert_matches!(&frames[1].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script2.sh");
785        assert_matches!(&frames[2].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script1.sh");
786    }
787}