Skip to content

Commit 2835487

Browse files
authored
refactor: extract script + function call stacks to their own modules (reubeno#709)
1 parent c5add15 commit 2835487

5 files changed

Lines changed: 353 additions & 53 deletions

File tree

brush-core/src/functions.rs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
//! Structures for managing function registrations.
1+
//! Structures for managing function registrations and calls.
22
3-
use std::{collections::HashMap, sync::Arc};
3+
use std::{
4+
collections::{HashMap, VecDeque},
5+
sync::Arc,
6+
};
7+
8+
use brush_parser::ast;
49

510
/// An environment for defined, named functions.
611
#[derive(Clone, Default)]
@@ -97,3 +102,76 @@ impl Registration {
97102
self.exported
98103
}
99104
}
105+
106+
/// Represents an active shell function call.
107+
#[derive(Clone, Debug)]
108+
pub struct FunctionCall {
109+
/// The name of the function invoked.
110+
pub function_name: String,
111+
/// The definition of the invoked function.
112+
pub function_definition: Arc<brush_parser::ast::FunctionDefinition>,
113+
}
114+
115+
/// Encapsulates a function call stack.
116+
#[derive(Clone, Debug, Default)]
117+
pub struct CallStack {
118+
frames: VecDeque<FunctionCall>,
119+
}
120+
121+
impl std::fmt::Display for CallStack {
122+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123+
if self.is_empty() {
124+
return Ok(());
125+
}
126+
127+
writeln!(f, "Function call stack (most recent first):")?;
128+
129+
for (index, frame) in self.iter().enumerate() {
130+
writeln!(f, " #{}| {}", index, frame.function_name)?;
131+
}
132+
133+
Ok(())
134+
}
135+
}
136+
137+
impl CallStack {
138+
/// Creates a new empty function call stack.
139+
pub fn new() -> Self {
140+
Self::default()
141+
}
142+
143+
/// Removes the top from from the stack. If the stack is empty, does nothing and
144+
/// returns `None`; otherwise, returns the removed call frame.
145+
pub fn pop(&mut self) -> Option<FunctionCall> {
146+
self.frames.pop_front()
147+
}
148+
149+
/// Pushes a new frame onto the stack.
150+
///
151+
/// # Arguments
152+
///
153+
/// * `name` - The name of the function being called.
154+
/// * `function_def` - The definition of the function being called.
155+
pub fn push(&mut self, name: impl Into<String>, function_def: &Arc<ast::FunctionDefinition>) {
156+
self.frames.push_front(FunctionCall {
157+
function_name: name.into(),
158+
function_definition: function_def.clone(),
159+
});
160+
}
161+
162+
/// Returns the current depth of the function call stack.
163+
pub fn depth(&self) -> usize {
164+
self.frames.len()
165+
}
166+
167+
/// Returns whether or not the function call stack is empty.
168+
pub fn is_empty(&self) -> bool {
169+
self.frames.is_empty()
170+
}
171+
172+
/// Returns an iterator over the function call frames, starting from the most
173+
/// recent.
174+
pub fn iter(&self) -> impl Iterator<Item = &FunctionCall> {
175+
self.frames.iter()
176+
}
177+
}

brush-core/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub mod patterns;
2727
pub mod processes;
2828
mod prompt;
2929
mod regex;
30+
pub mod scripts;
3031
mod shell;
3132
pub mod sys;
3233
mod terminal;
@@ -40,8 +41,6 @@ mod wellknownvars;
4041
pub use commands::{CommandArg, ExecutionContext};
4142
pub use error::Error;
4243
pub use interp::{ExecutionParameters, ExecutionResult, ProcessGroupPolicy};
43-
pub use shell::{
44-
CreateOptions, FunctionCall, ScriptCallType, Shell, ShellBuilder, ShellBuilderState,
45-
};
44+
pub use shell::{CreateOptions, Shell, ShellBuilder, ShellBuilderState};
4645
pub use terminal::TerminalControl;
4746
pub use variables::{ShellValue, ShellVariable};

brush-core/src/scripts.rs

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
//! Call stack representations.
2+
3+
use std::collections::VecDeque;
4+
5+
/// Represents an executing script.
6+
#[derive(Clone, Debug)]
7+
pub enum CallType {
8+
/// The script was sourced.
9+
Sourced,
10+
/// The script was executed.
11+
Executed,
12+
}
13+
14+
impl std::fmt::Display for CallType {
15+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16+
match self {
17+
Self::Sourced => write!(f, "sourced"),
18+
Self::Executed => write!(f, "executed"),
19+
}
20+
}
21+
}
22+
23+
/// Represents a single frame in a script call stack.
24+
#[derive(Clone, Debug)]
25+
pub struct CallFrame {
26+
/// The type of script call that resulted in this frame.
27+
pub call_type: CallType,
28+
/// The source of the script (e.g., file path).
29+
pub source: String,
30+
}
31+
32+
/// Encapsulates a script call stack.
33+
#[derive(Clone, Debug, Default)]
34+
pub struct CallStack {
35+
frames: VecDeque<CallFrame>,
36+
}
37+
38+
impl std::fmt::Display for CallStack {
39+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40+
if self.is_empty() {
41+
return Ok(());
42+
}
43+
44+
writeln!(f, "Script call stack (most recent first):")?;
45+
46+
for (index, frame) in self.iter().enumerate() {
47+
writeln!(f, " #{}| {} ({})", index, frame.source, frame.call_type)?;
48+
}
49+
50+
Ok(())
51+
}
52+
}
53+
54+
impl CallStack {
55+
/// Creates a new empty script call stack.
56+
pub fn new() -> Self {
57+
Self::default()
58+
}
59+
60+
/// Removes the top from from the stack. If the stack is empty, does nothing and
61+
/// returns `None`; otherwise, returns the removed call frame.
62+
pub fn pop(&mut self) -> Option<CallFrame> {
63+
self.frames.pop_front()
64+
}
65+
66+
/// Pushes a new frame onto the stack.
67+
///
68+
/// # Arguments
69+
///
70+
/// * `call_type` - The type of script call (sourced or executed).
71+
/// * `source` - The source of the script (e.g., file path).
72+
pub fn push(&mut self, call_type: CallType, source: impl Into<String>) {
73+
self.frames.push_front(CallFrame {
74+
call_type,
75+
source: source.into(),
76+
});
77+
}
78+
79+
/// Returns whether or not the current script stack frame is a sourced script.
80+
pub fn in_sourced_script(&self) -> bool {
81+
self.frames
82+
.front()
83+
.is_some_and(|frame| matches!(frame.call_type, CallType::Sourced))
84+
}
85+
86+
/// Returns the current depth of the script call stack.
87+
pub fn depth(&self) -> usize {
88+
self.frames.len()
89+
}
90+
91+
/// Returns whether or not the script call stack is empty.
92+
pub fn is_empty(&self) -> bool {
93+
self.frames.is_empty()
94+
}
95+
96+
/// Returns an iterator over the script call frames, starting from the most
97+
/// recent.
98+
pub fn iter(&self) -> impl Iterator<Item = &CallFrame> {
99+
self.frames.iter()
100+
}
101+
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use super::*;
106+
107+
#[test]
108+
fn test_call_type_display() {
109+
assert_eq!(CallType::Sourced.to_string(), "sourced");
110+
assert_eq!(CallType::Executed.to_string(), "executed");
111+
}
112+
113+
#[test]
114+
fn test_call_stack_new() {
115+
let stack = CallStack::new();
116+
assert!(stack.is_empty());
117+
assert_eq!(stack.depth(), 0);
118+
}
119+
120+
#[test]
121+
fn test_call_stack_default() {
122+
let stack = CallStack::default();
123+
assert!(stack.is_empty());
124+
assert_eq!(stack.depth(), 0);
125+
}
126+
127+
#[test]
128+
fn test_call_stack_push_pop() {
129+
let mut stack = CallStack::new();
130+
131+
stack.push(CallType::Sourced, "script1.sh");
132+
assert!(!stack.is_empty());
133+
assert_eq!(stack.depth(), 1);
134+
135+
stack.push(CallType::Executed, "script2.sh");
136+
assert_eq!(stack.depth(), 2);
137+
138+
let frame = stack.pop().unwrap();
139+
assert_eq!(frame.source, "script2.sh");
140+
assert!(matches!(frame.call_type, CallType::Executed));
141+
assert_eq!(stack.depth(), 1);
142+
143+
let frame = stack.pop().unwrap();
144+
assert_eq!(frame.source, "script1.sh");
145+
assert!(matches!(frame.call_type, CallType::Sourced));
146+
assert_eq!(stack.depth(), 0);
147+
assert!(stack.is_empty());
148+
}
149+
150+
#[test]
151+
fn test_call_stack_pop_empty() {
152+
let mut stack = CallStack::new();
153+
assert!(stack.pop().is_none());
154+
}
155+
156+
#[test]
157+
fn test_in_sourced_script() {
158+
let mut stack = CallStack::new();
159+
assert!(!stack.in_sourced_script());
160+
161+
stack.push(CallType::Executed, "script1.sh");
162+
assert!(!stack.in_sourced_script());
163+
164+
stack.push(CallType::Sourced, "script2.sh");
165+
assert!(stack.in_sourced_script());
166+
167+
stack.pop();
168+
assert!(!stack.in_sourced_script());
169+
}
170+
171+
#[test]
172+
fn test_call_stack_iter() {
173+
let mut stack = CallStack::new();
174+
stack.push(CallType::Sourced, "script1.sh");
175+
stack.push(CallType::Executed, "script2.sh");
176+
stack.push(CallType::Sourced, "script3.sh");
177+
178+
let frames: Vec<_> = stack.iter().collect();
179+
assert_eq!(frames.len(), 3);
180+
assert_eq!(frames[0].source, "script3.sh");
181+
assert_eq!(frames[1].source, "script2.sh");
182+
assert_eq!(frames[2].source, "script1.sh");
183+
}
184+
185+
#[test]
186+
fn test_call_stack_display_empty() {
187+
let stack = CallStack::new();
188+
assert_eq!(stack.to_string(), "");
189+
}
190+
191+
#[test]
192+
fn test_call_stack_display_with_frames() {
193+
let mut stack = CallStack::new();
194+
stack.push(CallType::Sourced, "script1.sh");
195+
stack.push(CallType::Executed, "script2.sh");
196+
197+
let output = stack.to_string();
198+
assert!(output.contains("Script call stack (most recent first):"));
199+
assert!(output.contains("#0| script2.sh (executed)"));
200+
assert!(output.contains("#1| script1.sh (sourced)"));
201+
}
202+
203+
#[test]
204+
fn test_call_frame_clone() {
205+
let frame1 = CallFrame {
206+
call_type: CallType::Sourced,
207+
source: "test.sh".to_string(),
208+
};
209+
let frame2 = frame1.clone();
210+
211+
assert_eq!(frame1.source, frame2.source);
212+
assert!(matches!(frame1.call_type, CallType::Sourced));
213+
assert!(matches!(frame2.call_type, CallType::Sourced));
214+
}
215+
216+
#[test]
217+
fn test_call_stack_clone() {
218+
let mut stack1 = CallStack::new();
219+
stack1.push(CallType::Sourced, "script1.sh");
220+
stack1.push(CallType::Executed, "script2.sh");
221+
222+
let stack2 = stack1.clone();
223+
assert_eq!(stack1.depth(), stack2.depth());
224+
225+
let frames1: Vec<_> = stack1.iter().map(|f| &f.source).collect();
226+
let frames2: Vec<_> = stack2.iter().map(|f| &f.source).collect();
227+
assert_eq!(frames1, frames2);
228+
}
229+
230+
#[test]
231+
fn test_push_with_string_types() {
232+
let mut stack = CallStack::new();
233+
234+
// Test with &str
235+
stack.push(CallType::Sourced, "script1.sh");
236+
237+
// Test with String
238+
stack.push(CallType::Executed, String::from("script2.sh"));
239+
240+
// Test with owned string reference
241+
let owned = "script3.sh".to_string();
242+
stack.push(CallType::Sourced, &owned);
243+
244+
assert_eq!(stack.depth(), 3);
245+
}
246+
}

0 commit comments

Comments
 (0)