feat(rome_cli): refactor the Termination type into a proper error type
MODIFY
Cargo.lock
Cargo.lock
@@ -1357,6 +1357,7 @@ dependencies = [
"rome_formatter",
"rome_fs",
"rome_js_parser",
+ "thiserror",
"tracing",
"tracing-subscriber",
]

MODIFY
crates/rome_cli/Cargo.toml
crates/rome_cli/Cargo.toml
@@ -18,3 +18,4 @@ parking_lot = "0.12.0"
lazy_static = "1.4.0"
hdrhistogram = { version = "7.5.0", default-features = false }
crossbeam = "0.8.1"
+thiserror = "1.0.30"

MODIFY
crates/rome_cli/src/commands/format.rs
crates/rome_cli/src/commands/format.rs
@@ -30,12 +30,18 @@ pub(crate) fn format(mut session: CliSession) -> Result<(), Termination> {
let size = session
.args
.opt_value_from_str("--indent-size")
- .expect("failed to parse indent-size argument");
+ .map_err(|source| Termination::ParseError {
+ argument: "--indent-size",
+ source,
+ })?;
let style = session
.args
.opt_value_from_str("--indent-style")
- .expect("failed to parse indent-style argument");
+ .map_err(|source| Termination::ParseError {
+ argument: "--indent-style",
+ source,
+ })?;
match style {
Some(IndentStyle::Tab) => {
@@ -62,18 +68,16 @@ pub(crate) fn format(mut session: CliSession) -> Result<(), Termination> {
}
// `--<some character>` or `-<some character>`
if without_dashes != input {
- let command_name = std::env::current_exe()
- .ok()
- .and_then(|path| Some(path.file_name()?.to_str()?.to_string()))
- .unwrap_or_else(|| String::from("rome"));
- panic!("unrecognized option {input:?}. Type '{command_name} format --help' for more information.");
+ return Err(Termination::UnexpectedArgument { argument: input });
}
}
inputs.push(input);
}
if inputs.is_empty() {
- panic!("needs at least one input file or directory");
+ return Err(Termination::MissingArgument {
+ argument: "<INPUT>",
+ });
}
let (interner, recv_files) = AtomicInterner::new();
@@ -174,7 +178,7 @@ pub(crate) fn format(mut session: CliSession) -> Result<(), Termination> {
if !has_errors {
Ok(())
} else {
- Err(Termination::from("errors where emitted while formatting"))
+ Err(Termination::FormattingError)
}
}

MODIFY
crates/rome_cli/src/commands/help.rs
crates/rome_cli/src/commands/help.rs
+use crate::Termination;
+
const MAIN: &str = concat!(
"Rome v",
env!("CARGO_PKG_VERSION"),
@@ -22,17 +24,19 @@ OPTIONS:
--indent-size <number> If the indentation style is set to spaces, determine how many spaces should be used for indentation (default: 2)
";
-pub(crate) fn help(command: Option<&str>) {
+pub(crate) fn help(command: Option<&str>) -> Result<(), Termination> {
match command {
None => {
- print!("{MAIN}")
+ print!("{MAIN}");
+ Ok(())
}
Some("format") => {
- print!("{FORMAT}")
+ print!("{FORMAT}");
+ Ok(())
}
- Some(cmd) => {
- panic!("cannot print help for unknown command {cmd:?}")
- }
+ Some(cmd) => Err(Termination::UnknownCommandHelp {
+ command: cmd.into(),
+ }),
}
}

MODIFY
crates/rome_cli/src/lib.rs
crates/rome_cli/src/lib.rs
-use std::fmt::{self, Debug, Display, Formatter};
-
use pico_args::Arguments;
use rome_core::App;
mod commands;
mod metrics;
mod panic;
+mod termination;
pub use panic::setup_panic_handler;
+pub use termination::Termination;
/// Global context for an execution of the CLI
pub struct CliSession {
@@ -26,22 +26,6 @@ impl CliSession {
}
}
-/// Error message returned by the CLI when it aborts with an error
-#[derive(PartialEq, Eq)]
-pub struct Termination(pub(crate) String);
-
-impl<T: Display> From<T> for Termination {
- fn from(msg: T) -> Self {
- Self(msg.to_string())
- }
-}
-
-impl Debug for Termination {
- fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
- write!(fmt, "{}", self.0)
- }
-}
-
/// Main function to run Rome CLI
pub fn run_cli(mut session: CliSession) -> Result<(), Termination> {
let has_metrics = session.args.contains("--show-metrics");
@@ -50,15 +34,18 @@ pub fn run_cli(mut session: CliSession) -> Result<(), Termination> {
}
let has_help = session.args.contains("--help");
- let subcommand = session.args.subcommand();
-
- match subcommand.as_ref().map(Option::as_deref) {
- Ok(Some(cmd)) if has_help => {
- crate::commands::help::help(Some(cmd));
- Ok(())
- }
-
- Ok(Some("format")) => {
+ let subcommand = session
+ .args
+ .subcommand()
+ .map_err(|source| Termination::ParseError {
+ argument: "<command>",
+ source,
+ })?;
+
+ match subcommand.as_deref() {
+ Some(cmd) if has_help => crate::commands::help::help(Some(cmd)),
+
+ Some("format") => {
let result = crate::commands::format::format(session);
if has_metrics {
@@ -68,12 +55,10 @@ pub fn run_cli(mut session: CliSession) -> Result<(), Termination> {
result
}
- Ok(None | Some("help")) => {
- crate::commands::help::help(None);
- Ok(())
- }
+ None | Some("help") => crate::commands::help::help(None),
- Ok(Some(cmd)) => Err(Termination(format!("unknown command {cmd:?}"))),
- Err(err) => Err(Termination(format!("failed to parse command: {err}"))),
+ Some(cmd) => Err(Termination::UnknownCommand {
+ command: cmd.into(),
+ }),
}
}

ADD
crates/rome_cli/src/termination.rs
+use std::{
+ env::current_exe,
+ ffi::OsString,
+ fmt::{self, Debug, Formatter},
+};
+use thiserror::Error;
+
+/// Error message returned by the CLI when it aborts with an error
+#[derive(Error)]
+pub enum Termination {
+ /// Returned when it is called with a subcommand it doesn't know
+ #[error("unknown command '{command}'")]
+ UnknownCommand { command: String },
+
+ /// Return by the help command when it is called with a subcommand it doesn't know
+ #[error("cannot print help for unknown command '{command}'")]
+ UnknownCommandHelp { command: String },
+
+ /// Returned when the value of a command line argument could not be parsed
+ #[error("failed to parse argument '{argument}': {source}")]
+ ParseError {
+ argument: &'static str,
+ #[source]
+ source: pico_args::Error,
+ },
+
+ /// Returned when the CLI doesn't recognize a command line argument
+ #[error(
+ "unrecognized option {argument:?}. Type '{} format --help' for more information.",
+ command_name()
+ )]
+ UnexpectedArgument { argument: OsString },
+
+ /// Returned when a required argument is not present in the command line
+ #[error(
+ "missing argument '{argument}'. Type '{} format --help' for more information.",
+ command_name()
+ )]
+ MissingArgument { argument: &'static str },
+
+ /// Returned by the formatter when error diagnostics were emitted in CI mode
+ #[error("errors where emitted while formatting")]
+ FormattingError,
+}
+
+fn command_name() -> String {
+ current_exe()
+ .ok()
+ .and_then(|path| Some(path.file_name()?.to_str()?.to_string()))
+ .unwrap_or_else(|| String::from("rome"))
+}
+
+// Termination implements Debug by redirecting to Display instead of deriving
+// a "canonical" debug implementation as it it is returned as a Result in the
+// main function and gets printed by the standard library, which uses Debug but
+// we want to show the actuall error message to the user in case of an error
+impl Debug for Termination {
+ fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
+ write!(fmt, "{}", self)
+ }
+}

MODIFY
crates/rome_cli/tests/main.rs
crates/rome_cli/tests/main.rs
use std::{ffi::OsString, path::Path, sync::Arc};
use pico_args::Arguments;
-use rome_cli::{run_cli, CliSession};
+use rome_cli::{run_cli, CliSession, Termination};
use rome_core::App;
use rome_fs::{FileSystem, MemoryFileSystem};
@@ -18,7 +18,7 @@ fn test_format_cli() {
args: Arguments::from_vec(vec![OsString::from("format"), file_path.as_os_str().into()]),
});
- assert_eq!(result, Ok(()));
+ assert!(result.is_ok(), "run_cli returned {result:?}");
let mut file = fs
.open(file_path)
@@ -30,3 +30,122 @@ fn test_format_cli() {
assert_eq!(content, "statement();\n");
}
+
+#[test]
+fn test_unknown_command() {
+ let result = run_cli(CliSession {
+ app: App::with_filesystem(MemoryFileSystem::default()),
+ args: Arguments::from_vec(vec![OsString::from("unknown")]),
+ });
+
+ match result {
+ Err(Termination::UnknownCommand { command }) => assert_eq!(command, "unknown"),
+ _ => panic!("run_cli returned {result:?} for an unknown command, expected an error"),
+ }
+}
+
+#[test]
+fn test_unknown_command_help() {
+ let result = run_cli(CliSession {
+ app: App::with_filesystem(MemoryFileSystem::default()),
+ args: Arguments::from_vec(vec![OsString::from("unknown"), OsString::from("--help")]),
+ });
+
+ match result {
+ Err(Termination::UnknownCommandHelp { command }) => assert_eq!(command, "unknown"),
+ _ => panic!("run_cli returned {result:?} for an unknown command help, expected an error"),
+ }
+}
+
+#[test]
+fn test_indent_style_parse_errors() {
+ let result = run_cli(CliSession {
+ app: App::with_filesystem(MemoryFileSystem::default()),
+ args: Arguments::from_vec(vec![
+ OsString::from("format"),
+ OsString::from("--indent-style"),
+ OsString::from("invalid"),
+ OsString::from("file.js"),
+ ]),
+ });
+
+ match result {
+ Err(Termination::ParseError { argument, .. }) => assert_eq!(argument, "--indent-style"),
+ _ => panic!("run_cli returned {result:?} for an invalid argument value, expected an error"),
+ }
+}
+
+#[test]
+fn test_indent_size_parse_errors() {
+ let result = run_cli(CliSession {
+ app: App::with_filesystem(MemoryFileSystem::default()),
+ args: Arguments::from_vec(vec![
+ OsString::from("format"),
+ OsString::from("--indent-size"),
+ OsString::from("-1"),
+ OsString::from("file.js"),
+ ]),
+ });
+
+ match result {
+ Err(Termination::ParseError { argument, .. }) => assert_eq!(argument, "--indent-size"),
+ _ => panic!("run_cli returned {result:?} for an invalid argument value, expected an error"),
+ }
+}
+
+#[test]
+fn test_unexpected_argument() {
+ let result = run_cli(CliSession {
+ app: App::with_filesystem(MemoryFileSystem::default()),
+ args: Arguments::from_vec(vec![
+ OsString::from("format"),
+ OsString::from("--unknown"),
+ OsString::from("file.js"),
+ ]),
+ });
+
+ match result {
+ Err(Termination::UnexpectedArgument { argument, .. }) => {
+ assert_eq!(argument, OsString::from("--unknown"))
+ }
+ _ => panic!("run_cli returned {result:?} for an unknown argument, expected an error"),
+ }
+}
+
+#[test]
+fn test_missing_argument() {
+ let result = run_cli(CliSession {
+ app: App::with_filesystem(MemoryFileSystem::default()),
+ args: Arguments::from_vec(vec![OsString::from("format")]),
+ });
+
+ match result {
+ Err(Termination::MissingArgument { argument }) => assert_eq!(argument, "<INPUT>"),
+ _ => panic!("run_cli returned {result:?} for a missing argument, expected an error"),
+ }
+}
+
+#[test]
+fn test_formatting_error() {
+ let mut fs = MemoryFileSystem::default();
+
+ let file_path = Path::new("format.js");
+ fs.insert(
+ file_path.into(),
+ b" unformatted_statement( ) ".as_slice(),
+ );
+
+ let result = run_cli(CliSession {
+ app: App::with_filesystem(fs),
+ args: Arguments::from_vec(vec![
+ OsString::from("format"),
+ OsString::from("--ci"),
+ file_path.as_os_str().into(),
+ ]),
+ });
+
+ match result {
+ Err(Termination::FormattingError) => {}
+ _ => panic!("run_cli returned {result:?} for a failed CI check, expected an error"),
+ }
+}

feat(rome_cli): refactor the Termination type into a proper error type (#2301)