Skip to main content

chisel/
args.rs

1use crate::{
2    opts::{Chisel, ChiselSubcommand},
3    prelude::{ChiselCommand, ChiselDispatcher, SolidityHelper},
4};
5use clap::Parser;
6use eyre::{Context, Result};
7use foundry_cli::utils::{self, LoadConfig};
8use foundry_common::fs;
9use rustyline::{Editor, config::Configurer, error::ReadlineError};
10use std::{ops::ControlFlow, path::PathBuf};
11use yansi::Paint;
12
13/// Run the `chisel` command line interface.
14pub fn run() -> Result<()> {
15    // Pre-parse discovery flags run before `setup()` so they cannot be blocked
16    // by panic-handler / tracing init failures and avoid that init's cost.
17    foundry_cli::opts::GlobalArgs::check_introspect::<Chisel>();
18    foundry_cli::opts::GlobalArgs::check_markdown_help::<Chisel>();
19
20    setup()?;
21
22    let args = Chisel::parse();
23    args.global.init()?;
24    args.global.tokio_runtime().block_on(run_command(args))
25}
26
27/// Setup the global logger and other utilities.
28pub fn setup() -> Result<()> {
29    utils::common_setup();
30    utils::subscriber();
31
32    Ok(())
33}
34
35macro_rules! try_cf {
36    ($e:expr) => {
37        match $e {
38            ControlFlow::Continue(()) => {}
39            ControlFlow::Break(()) => return Ok(()),
40        }
41    };
42}
43
44/// Run the subcommand.
45pub async fn run_command(args: Chisel) -> Result<()> {
46    // Load configuration
47    let (config, evm_opts) = args.load_config_and_evm_opts()?;
48
49    // Create a new cli dispatcher
50    let mut dispatcher = ChiselDispatcher::new(crate::source::SessionSourceConfig {
51        // Enable traces if any level of verbosity was passed
52        traces: config.verbosity > 0,
53        foundry_config: config,
54        no_vm: args.no_vm,
55        evm_opts,
56        backend: None,
57        calldata: None,
58        ir_minimum: args.ir_minimum,
59    })?;
60
61    // Execute prelude Solidity source files
62    evaluate_prelude(&mut dispatcher, args.prelude).await?;
63
64    if let Some(cmd) = args.cmd {
65        try_cf!(handle_cli_command(&mut dispatcher, cmd).await?);
66        return Ok(());
67    }
68
69    let mut rl = Editor::<SolidityHelper, _>::new()?;
70    rl.set_helper(Some(dispatcher.helper.clone()));
71    rl.set_auto_add_history(true);
72    if let Some(path) = chisel_history_file() {
73        let _ = rl.load_history(&path);
74    }
75
76    sh_println!("Welcome to Chisel! Type `{}` to show available commands.", "!help".green())?;
77
78    // REPL loop.
79    let mut interrupt = false;
80    loop {
81        match rl.readline(&dispatcher.get_prompt()) {
82            Ok(line) => {
83                debug!("dispatching next line: {line}");
84                // Clear interrupt flag.
85                interrupt = false;
86
87                // Dispatch and match results.
88                let r = dispatcher.dispatch(&line).await;
89                dispatcher.helper.set_errored(r.is_err());
90                match r {
91                    Ok(ControlFlow::Continue(())) => {}
92                    Ok(ControlFlow::Break(())) => break,
93                    Err(e) => {
94                        sh_err!("{}", foundry_common::errors::display_chain(&e))?;
95                    }
96                }
97            }
98            Err(ReadlineError::Interrupted) => {
99                if interrupt {
100                    break;
101                }
102                sh_println!("(To exit, press Ctrl+C again)")?;
103                interrupt = true;
104            }
105            Err(ReadlineError::Eof) => break,
106            Err(err) => {
107                sh_err!("{err}")?;
108                break;
109            }
110        }
111    }
112
113    if let Some(path) = chisel_history_file() {
114        let _ = rl.save_history(&path);
115    }
116
117    Ok(())
118}
119
120/// Evaluate multiple Solidity source files contained within a
121/// Chisel prelude directory.
122async fn evaluate_prelude(
123    dispatcher: &mut ChiselDispatcher,
124    maybe_prelude: Option<PathBuf>,
125) -> Result<()> {
126    let Some(prelude_dir) = maybe_prelude else { return Ok(()) };
127    if prelude_dir.is_file() {
128        sh_println!("{} {}", "Loading prelude source file:".yellow(), prelude_dir.display())?;
129        try_cf!(load_prelude_file(dispatcher, prelude_dir).await?);
130        sh_println!("{}\n", "Prelude source file loaded successfully!".green())?;
131    } else {
132        let prelude_sources = fs::files_with_ext(&prelude_dir, "sol");
133        let mut print_success_msg = false;
134        for source_file in prelude_sources {
135            print_success_msg = true;
136            sh_println!("{} {}", "Loading prelude source file:".yellow(), source_file.display())?;
137            try_cf!(load_prelude_file(dispatcher, source_file).await?);
138        }
139
140        if print_success_msg {
141            sh_println!("{}\n", "All prelude source files loaded successfully!".green())?;
142        }
143    }
144    Ok(())
145}
146
147/// Loads a single Solidity file into the prelude.
148async fn load_prelude_file(
149    dispatcher: &mut ChiselDispatcher,
150    file: PathBuf,
151) -> Result<ControlFlow<()>> {
152    let prelude = fs::read_to_string(file)
153        .wrap_err("Could not load source file. Are you sure this path is correct?")?;
154    dispatcher.dispatch(&prelude).await
155}
156
157async fn handle_cli_command(
158    d: &mut ChiselDispatcher,
159    cmd: ChiselSubcommand,
160) -> Result<ControlFlow<()>> {
161    match cmd {
162        ChiselSubcommand::List => d.dispatch_command(ChiselCommand::ListSessions).await,
163        ChiselSubcommand::Load { id } => d.dispatch_command(ChiselCommand::Load { id }).await,
164        ChiselSubcommand::View { id } => {
165            let ControlFlow::Continue(()) = d.dispatch_command(ChiselCommand::Load { id }).await?
166            else {
167                return Ok(ControlFlow::Break(()));
168            };
169            d.dispatch_command(ChiselCommand::Source).await
170        }
171        ChiselSubcommand::ClearCache => d.dispatch_command(ChiselCommand::ClearCache).await,
172        ChiselSubcommand::Eval { command } => d.dispatch(&command).await,
173    }
174}
175
176fn chisel_history_file() -> Option<PathBuf> {
177    foundry_config::Config::foundry_dir().map(|p| p.join(".chisel_history"))
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use clap::CommandFactory;
184
185    #[test]
186    fn verify_cli() {
187        Chisel::command().debug_assert();
188    }
189
190    /// Every `command_id` exposed by `chisel --introspect` MUST be unique.
191    #[test]
192    fn introspect_command_ids_are_unique() {
193        use foundry_cli::introspect::{CommandRegistry, build_document, duplicate_command_ids};
194        let cmd = Chisel::command();
195        let doc = build_document(&cmd, &CommandRegistry::EMPTY);
196        let dups = duplicate_command_ids(&doc);
197        assert!(dups.is_empty(), "duplicate chisel command_ids: {dups:?}");
198    }
199
200    /// `chisel --introspect` must produce a JSON document that parses back into
201    /// the canonical `IntrospectDocument` shape.
202    #[test]
203    fn introspect_document_is_valid_json() {
204        use foundry_cli::introspect::{
205            CommandRegistry, INTROSPECT_SCHEMA_ID, IntrospectDocument, render_introspect_document,
206        };
207        let cmd = Chisel::command();
208        let json = render_introspect_document(&cmd, &CommandRegistry::EMPTY);
209        let doc: IntrospectDocument = serde_json::from_str(&json).expect("valid JSON");
210        assert_eq!(doc.schema_id, INTROSPECT_SCHEMA_ID);
211        assert_eq!(doc.binary.name, "chisel");
212    }
213}