feat(rome_cli): read configuration file from CLI
MODIFY
crates/rome_cli/src/commands/ci.rs
crates/rome_cli/src/commands/ci.rs
@@ -2,11 +2,15 @@ use crate::{
traversal::{traverse, TraversalMode},
CliSession, Termination,
};
+use rome_service::load_config;
+use rome_service::load_config::ConfigurationType;
use super::format::parse_format_options;
/// Handler for the "ci" command of the Rome CLI
pub(crate) fn ci(mut session: CliSession) -> Result<(), Termination> {
- parse_format_options(&mut session)?;
+ let configuration = load_config(&session.app.fs, ConfigurationType::Root)?;
+
+ parse_format_options(&mut session, configuration)?;
traverse(TraversalMode::CI, session)
}

MODIFY
crates/rome_cli/src/commands/format.rs
crates/rome_cli/src/commands/format.rs
use rome_formatter::IndentStyle;
-use rome_service::{settings::WorkspaceSettings, workspace::UpdateSettingsParams};
+use rome_service::configuration::Configuration;
+use rome_service::load_config::ConfigurationType;
+use rome_service::settings::FormatSettings;
+use rome_service::{load_config, settings::WorkspaceSettings, workspace::UpdateSettingsParams};
use crate::{
traversal::{traverse, TraversalMode},
@@ -8,7 +11,14 @@ use crate::{
/// Handler for the "format" command of the Rome CLI
pub(crate) fn format(mut session: CliSession) -> Result<(), Termination> {
- parse_format_options(&mut session)?;
+ let configuration = load_config(&session.app.fs, ConfigurationType::Root)?;
+
+ if let Some(configuration) = &configuration {
+ if configuration.is_formatter_disabled() {
+ return Ok(());
+ }
+ }
+ parse_format_options(&mut session, configuration)?;
let is_write = session.args.contains("--write");
let ignore_errors = session.args.contains("--skip-errors");
@@ -24,9 +34,18 @@ pub(crate) fn format(mut session: CliSession) -> Result<(), Termination> {
/// Read the formatting options for the command line arguments and inject them
/// into the workspace settings
-pub(crate) fn parse_format_options(session: &mut CliSession) -> Result<(), Termination> {
+pub(crate) fn parse_format_options(
+ session: &mut CliSession,
+ configuration: Option<Configuration>,
+) -> Result<(), Termination> {
let mut settings = WorkspaceSettings::default();
+ if let Some(configuration) = configuration {
+ settings.format = FormatSettings::from(&configuration.formatter);
+ settings.languages.javascript.format.quote_style =
+ Some(configuration.javascript.formatter.quote_style);
+ }
+
let size = session
.args
.opt_value_from_str("--indent-size")

ADD
crates/rome_cli/tests/configs.rs
+pub const CONFIG_FORMAT: &str = r#"{
+ "root": true,
+ "formatter": {
+ "enabled": true,
+ "lineWidth": 160,
+ "indentStyle": "space",
+ "indentSize": 6
+ }
+}
+"#;
+
+pub const CONFIG_DISABLED_FORMATTER: &str = r#"{
+ "root": true,
+ "formatter": {
+ "enabled": false
+ }
+}
+"#;
+
+pub const CONFIG_ROOT_FALSE: &str = r#"{
+ "root": false
+}
+"#;
+
+pub const CONFIG_ALL_FIELDS: &str = r#"{
+ "root": true,
+ "formatter": {
+ "enabled": true,
+ "formatWithErrors": true,
+ "indentStyle": "tab",
+ "indentSize": 2,
+ "lineWidth": 80
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "double"
+ }
+ }
+}"#;
+
+pub const CONFIG_BAD_LINE_WIDTH: &str = r#"{
+ "root": true,
+ "formatter": {
+ "lineWidth": 500
+ },
+}"#;

MODIFY
crates/rome_cli/tests/main.rs
crates/rome_cli/tests/main.rs
+mod configs;
+
use std::{ffi::OsString, path::Path};
use pico_args::Arguments;
@@ -40,6 +42,18 @@ var h;
var i;
";
+const CUSTOM_FORMAT_BEFORE: &str = r#"
+function f() {
+return { something }
+}
+"#;
+
+// six spaces
+const CUSTOM_FORMAT_AFTER: &str = r#"function f() {
+ return { something };
+}
+"#;
+
mod check {
use super::*;
use rome_console::LogLevel;
@@ -305,6 +319,7 @@ mod ci {
mod format {
use super::*;
+ use crate::configs::{CONFIG_DISABLED_FORMATTER, CONFIG_FORMAT};
#[test]
fn print() {
@@ -520,6 +535,74 @@ mod format {
),
}
}
+
+ #[test]
+ fn format_with_configuration() {
+ let mut fs = MemoryFileSystem::default();
+ let file_path = Path::new("rome.json");
+ fs.insert(file_path.into(), CONFIG_FORMAT.as_bytes());
+
+ let file_path = Path::new("file.js");
+ fs.insert(file_path.into(), CUSTOM_FORMAT_BEFORE.as_bytes());
+
+ let result = run_cli(CliSession {
+ app: App::with_filesystem_and_console(
+ DynRef::Borrowed(&mut fs),
+ DynRef::Owned(Box::new(BufferConsole::default())),
+ ),
+ args: Arguments::from_vec(vec![
+ OsString::from("format"),
+ OsString::from("file.js"),
+ OsString::from("--write"),
+ ]),
+ });
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ let mut file = fs
+ .open(file_path)
+ .expect("formatting target file was removed by the CLI");
+
+ let mut content = String::new();
+ file.read_to_string(&mut content)
+ .expect("failed to read file from memory FS");
+
+ assert_eq!(content, CUSTOM_FORMAT_AFTER);
+ }
+
+ #[test]
+ fn format_is_disabled() {
+ let mut fs = MemoryFileSystem::default();
+ let file_path = Path::new("rome.json");
+ fs.insert(file_path.into(), CONFIG_DISABLED_FORMATTER.as_bytes());
+
+ let file_path = Path::new("file.js");
+ fs.insert(file_path.into(), CUSTOM_FORMAT_BEFORE.as_bytes());
+
+ let result = run_cli(CliSession {
+ app: App::with_filesystem_and_console(
+ DynRef::Borrowed(&mut fs),
+ DynRef::Owned(Box::new(BufferConsole::default())),
+ ),
+ args: Arguments::from_vec(vec![
+ OsString::from("format"),
+ OsString::from("file.js"),
+ OsString::from("--write"),
+ ]),
+ });
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ let mut file = fs
+ .open(file_path)
+ .expect("formatting target file was removed by the CLI");
+
+ let mut content = String::new();
+ file.read_to_string(&mut content)
+ .expect("failed to read file from memory FS");
+
+ assert_eq!(content, CUSTOM_FORMAT_BEFORE);
+ }
}
mod help {
@@ -660,3 +743,84 @@ mod main {
}
}
}
+
+mod configuration {
+ use crate::configs::{CONFIG_ALL_FIELDS, CONFIG_BAD_LINE_WIDTH, CONFIG_ROOT_FALSE};
+ use pico_args::Arguments;
+ use rome_cli::{run_cli, CliSession};
+ use rome_console::BufferConsole;
+ use rome_fs::MemoryFileSystem;
+ use rome_service::{App, DynRef};
+ use std::ffi::OsString;
+ use std::path::Path;
+
+ #[test]
+ fn incorrect_root() {
+ let mut fs = MemoryFileSystem::default();
+ let file_path = Path::new("rome.json");
+ fs.insert(file_path.into(), CONFIG_ROOT_FALSE.as_bytes());
+
+ let result = run_cli(CliSession {
+ app: App::with_filesystem_and_console(
+ DynRef::Borrowed(&mut fs),
+ DynRef::Owned(Box::new(BufferConsole::default())),
+ ),
+ args: Arguments::from_vec(vec![OsString::from("format"), OsString::from("file.js")]),
+ });
+
+ assert!(result.is_err());
+
+ match result {
+ Err(error) => {
+ assert_eq!(
+ error.to_string(),
+ "the main configuration file, rome.json, must have the field 'root' set to `true`"
+ )
+ }
+ _ => panic!("expected an error, but found none"),
+ }
+ }
+
+ #[test]
+ fn correct_root() {
+ let mut fs = MemoryFileSystem::default();
+ let file_path = Path::new("rome.json");
+ fs.insert(file_path.into(), CONFIG_ALL_FIELDS.as_bytes());
+
+ let result = run_cli(CliSession {
+ app: App::with_filesystem_and_console(
+ DynRef::Borrowed(&mut fs),
+ DynRef::Owned(Box::new(BufferConsole::default())),
+ ),
+ args: Arguments::from_vec(vec![OsString::from("format"), OsString::from("file.js")]),
+ });
+
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn line_width_error() {
+ let mut fs = MemoryFileSystem::default();
+
+ let file_path = Path::new("rome.json");
+ fs.insert(file_path.into(), CONFIG_BAD_LINE_WIDTH.as_bytes());
+
+ let result = run_cli(CliSession {
+ app: App::with_filesystem_and_console(
+ DynRef::Borrowed(&mut fs),
+ DynRef::Owned(Box::new(BufferConsole::default())),
+ ),
+ args: Arguments::from_vec(vec![OsString::from("format"), OsString::from("file.js")]),
+ });
+ assert!(result.is_err());
+
+ match result {
+ Err(error) => {
+ assert!(error
+ .to_string()
+ .contains("The line width exceeds the maximum value (320)"),)
+ }
+ _ => panic!("expected an error, but found none"),
+ }
+ }
+}

MODIFY
crates/rome_fs/src/fs.rs
crates/rome_fs/src/fs.rs
@@ -12,6 +12,7 @@ mod os;
pub use memory::MemoryFileSystem;
pub use os::OsFileSystem;
+pub const CONFIG_NAME: &str = "rome.json";
pub trait FileSystem: Sync + RefUnwindSafe {
/// Open a handle to the file at `path`
@@ -24,6 +25,11 @@ pub trait FileSystem: Sync + RefUnwindSafe {
/// This method creates a new "traversal scope" that can be used to
/// efficiently batch many filesystem read operations
fn traversal<'scope>(&'scope self, func: BoxedTraversal<'_, 'scope>);
+
+ /// Returns the name of the main configuration file
+ fn config_name(&self) -> &str {
+ CONFIG_NAME
+ }
}
pub trait File {

MODIFY
crates/rome_fs/src/fs/os.rs
crates/rome_fs/src/fs/os.rs
//! Implementation of the [FileSystem] and related traits for the underlying OS filesystem
+use super::{BoxedTraversal, File};
+use crate::{
+ fs::{TraversalContext, TraversalScope},
+ interner::FileId,
+ FileSystem, RomePath,
+};
+use rayon::{scope, Scope};
use std::{
ffi::OsStr,
fs,
@@ -7,16 +14,6 @@ use std::{
path::{Path, PathBuf},
};
-use rayon::{scope, Scope};
-
-use crate::{
- fs::{TraversalContext, TraversalScope},
- interner::FileId,
- FileSystem, RomePath,
-};
-
-use super::{BoxedTraversal, File};
-
/// Implementation of [FileSystem] that directly calls through to the underlying OS
pub struct OsFileSystem;

MODIFY
crates/rome_service/src/configuration/formatter.rs
crates/rome_service/src/configuration/formatter.rs
+use crate::settings::FormatSettings;
use rome_formatter::{IndentStyle, LineWidth};
use serde::Deserialize;
+
#[derive(Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase", default)]
pub struct FormatterConfiguration {
@@ -21,11 +23,28 @@ pub struct FormatterConfiguration {
pub line_width: LineWidth,
}
-impl From<&FormatterConfiguration> for IndentStyle {
- fn from(c: &FormatterConfiguration) -> Self {
- match c.indent_style {
+impl Default for FormatterConfiguration {
+ fn default() -> Self {
+ Self {
+ enabled: true,
+ format_with_errors: false,
+ indent_size: 2,
+ indent_style: PlainIndentStyle::default(),
+ line_width: LineWidth::default(),
+ }
+ }
+}
+
+impl From<&FormatterConfiguration> for FormatSettings {
+ fn from(conf: &FormatterConfiguration) -> Self {
+ let indent_style = match conf.indent_style {
PlainIndentStyle::Tab => IndentStyle::Tab,
- PlainIndentStyle::Space => IndentStyle::Space(c.indent_size),
+ PlainIndentStyle::Space => IndentStyle::Space(conf.indent_size),
+ };
+ Self {
+ indent_style: Some(indent_style),
+ line_width: Some(conf.line_width),
+ format_with_errors: false,
}
}
}
@@ -38,18 +57,6 @@ where
LineWidth::try_from(value).map_err(serde::de::Error::custom)
}
-impl Default for FormatterConfiguration {
- fn default() -> Self {
- Self {
- enabled: true,
- format_with_errors: false,
- indent_size: 2,
- indent_style: PlainIndentStyle::default(),
- line_width: LineWidth::default(),
- }
- }
-}
-
#[derive(Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub enum PlainIndentStyle {

MODIFY
crates/rome_service/src/configuration/javascript.rs
crates/rome_service/src/configuration/javascript.rs
@@ -10,11 +10,12 @@ pub struct JavascriptConfiguration {
#[serde(rename_all = "camelCase", default)]
pub struct JavascriptFormatter {
/// The style for quotes. Defaults to double.
- pub quote_style: PlainQuoteStyle,
+ #[serde(with = "PlainQuoteStyle")]
+ pub quote_style: QuoteStyle,
}
#[derive(Deserialize, Debug, Eq, PartialEq)]
-#[serde(rename_all = "camelCase")]
+#[serde(rename_all = "camelCase", remote = "QuoteStyle")]
pub enum PlainQuoteStyle {
Double,
Single,
@@ -25,12 +26,3 @@ impl Default for PlainQuoteStyle {
Self::Double
}
}
-
-impl From<JavascriptConfiguration> for QuoteStyle {
- fn from(j: JavascriptConfiguration) -> Self {
- match j.formatter.quote_style {
- PlainQuoteStyle::Double => QuoteStyle::Double,
- PlainQuoteStyle::Single => QuoteStyle::Single,
- }
- }
-}

MODIFY
crates/rome_service/src/configuration/mod.rs
crates/rome_service/src/configuration/mod.rs
use crate::configuration::formatter::FormatterConfiguration;
use crate::configuration::javascript::JavascriptConfiguration;
use serde::Deserialize;
+use std::fmt::{Debug, Display, Formatter};
mod formatter;
mod javascript;
@@ -25,3 +26,36 @@ pub struct Configuration {
/// Specific configuration for the JavaScript language
pub javascript: JavascriptConfiguration,
}
+
+impl Configuration {
+ pub fn is_formatter_disabled(&self) -> bool {
+ !self.formatter.enabled
+ }
+}
+
+/// Series of errors that can be thrown while computing the configuration
+pub enum ConfigurationError {
+ /// Thrown when the main configuration file doesn't have
+ NotRoot,
+}
+
+impl Debug for ConfigurationError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ConfigurationError::NotRoot => std::fmt::Display::fmt(self, f),
+ }
+ }
+}
+
+impl Display for ConfigurationError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ConfigurationError::NotRoot => {
+ write!(
+ f,
+ "the main configuration file, rome.json, must have the field 'root' set to `true`"
+ )
+ }
+ }
+ }
+}

MODIFY
crates/rome_service/src/lib.rs
crates/rome_service/src/lib.rs
+use crate::configuration::ConfigurationError;
use rome_console::{Console, EnvConsole};
use rome_formatter::FormatError;
use rome_fs::{FileSystem, OsFileSystem, RomePath};
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::ops::{Deref, DerefMut};
+use std::path::PathBuf;
pub mod configuration;
mod file_handlers;
+pub mod load_config;
pub mod settings;
pub mod workspace;
pub use crate::file_handlers::JsFormatSettings;
+pub use crate::load_config::load_config;
pub use crate::workspace::Workspace;
pub struct App<'app> {
pub fs: DynRef<'app, dyn FileSystem>,
pub workspace: DynRef<'app, dyn Workspace>,
pub console: DynRef<'app, dyn Console>,
+ pub config_path: Option<RomePath>,
}
/// Generic errors thrown during rome operations
@@ -30,6 +35,17 @@ pub enum RomeError {
FormatError(FormatError),
/// The file could not be formatted since it has syntax errors and `format_with_errors` is disabled
FormatWithErrorsDisabled,
+ /// Thrown when a rome can't read a generic directory
+ CantReadDirectory(PathBuf),
+ /// Thrown when a rome can't read a generic file
+ CantReadFile(PathBuf),
+ /// Error thrown when de-serialising the configuration from file, the issues can be many:
+ /// - syntax error
+ /// - incorrect fields
+ /// - incorrect values
+ MalformedConfigurationFile(String),
+ /// Error thrown when validating the configuration. Once deserialized, further checks have to be done.
+ InvalidConfiguration(ConfigurationError),
}
impl Debug for RomeError {
@@ -39,6 +55,10 @@ impl Debug for RomeError {
RomeError::SourceFileNotSupported(_) => std::fmt::Display::fmt(self, f),
RomeError::FormatError(_) => std::fmt::Display::fmt(self, f),
RomeError::FormatWithErrorsDisabled => std::fmt::Display::fmt(self, f),
+ RomeError::CantReadDirectory(_) => std::fmt::Display::fmt(self, f),
+ RomeError::CantReadFile(_) => std::fmt::Display::fmt(self, f),
+ RomeError::MalformedConfigurationFile(_) => std::fmt::Display::fmt(self, f),
+ RomeError::InvalidConfiguration(_) => std::fmt::Display::fmt(self, f),
}
}
}
@@ -67,6 +87,29 @@ impl Display for RomeError {
RomeError::FormatWithErrorsDisabled => {
write!(f, "the file could not be formatted since it has syntax errors and `format_with_errors` is disabled")
}
+ RomeError::CantReadDirectory(path) => {
+ write!(
+ f,
+ "Rome couldn't read the following directory, maybe for permissions reasons or it doesn't exists: {}",
+ path.display()
+ )
+ }
+ RomeError::CantReadFile(path) => {
+ write!(
+ f,
+ "Rome couldn't read the following file, maybe for permissions reasons or it doesn't exists: {}",
+ path.display()
+ )
+ }
+ RomeError::MalformedConfigurationFile(reason) => {
+ write!(
+ f,
+ "Rome couldn't load the configuration file, here's why: \n{}",
+ reason
+ )
+ }
+
+ RomeError::InvalidConfiguration(error) => std::fmt::Display::fmt(error, f),
}
}
}
@@ -79,7 +122,7 @@ impl From<FormatError> for RomeError {
}
}
-impl App<'static> {
+impl<'app> App<'app> {
/// Create a new instance of the app using the [OsFileSystem] and [EnvConsole]
pub fn from_env(no_colors: bool) -> Self {
Self::with_filesystem_and_console(
@@ -99,6 +142,7 @@ impl<'app> App<'app> {
fs,
console,
workspace: DynRef::Owned(workspace::server()),
+ config_path: None,
}
}
}

ADD
crates/rome_service/src/load_config.rs
+use crate::configuration::{Configuration, ConfigurationError};
+use crate::{DynRef, RomeError};
+use rome_fs::FileSystem;
+use std::io::ErrorKind;
+use std::path::PathBuf;
+
+/// This function is responsible to load the rome configuration.
+///
+/// The `file_system` will read the configuration file
+pub fn load_config(
+ file_system: &DynRef<dyn FileSystem>,
+ configuration_type: ConfigurationType,
+) -> Result<Option<Configuration>, RomeError> {
+ let config_name = file_system.config_name();
+ let configuration_path = PathBuf::from(config_name);
+ let file = file_system.open(&configuration_path);
+
+ match file {
+ Ok(mut file) => {
+ let mut buffer = String::new();
+ file.read_to_string(&mut buffer).ok();
+
+ let configuration: Configuration = serde_json::from_str(&buffer)
+ .map_err(|err| RomeError::MalformedConfigurationFile(err.to_string()))?;
+
+ compute_configuration(configuration, configuration_type)
+ }
+ Err(err) => {
+ // we throw an error only when we can't find a file
+ if err.kind() != ErrorKind::NotFound {
+ return Err(RomeError::CantReadFile(configuration_path));
+ }
+ Ok(None)
+ }
+ }
+}
+
+/// The type of configuration we want to load
+pub enum ConfigurationType {
+ /// The main configuration, usually `rome.json`
+ Root,
+ /// The extended configuration, usually to be loaded via `extends` field
+ #[allow(unused_imports)]
+ Extended,
+}
+
+impl ConfigurationType {
+ fn is_root(&self) -> bool {
+ matches!(self, ConfigurationType::Root)
+ }
+}
+
+/// This function computes the configuration that is being loaded and makes sure that is correct.
+///
+/// Operations are:
+/// - making sure that the master configuration is set to `root: true`
+fn compute_configuration(
+ configuration: Configuration,
+ configuration_type: ConfigurationType,
+) -> Result<Option<Configuration>, RomeError> {
+ if configuration_type.is_root() && !configuration.root {
+ return Err(RomeError::InvalidConfiguration(ConfigurationError::NotRoot));
+ }
+
+ Ok(Some(configuration))
+}

DELETE
crates/rome_service/tests/all_fields.json
-{
- "root": true,
- "formatter": {
- "enabled": true,
- "formatWithErrors": true,
- "indentStyle": "tab",
- "indentSize": 2,
- "lineWidth": 80
- },
- "javascript": {
- "formatter": {
- "quoteStyle": "double"
- }
- }
-}
\ No newline at end of file

DELETE
crates/rome_service/tests/empty.json
-{}
\ No newline at end of file

DELETE
crates/rome_service/tests/line_width.json
-{
- "root": true,
- "formatter": {
- "lineWidth": 450
- }
-}
\ No newline at end of file

DELETE
crates/rome_service/tests/mod.rs
-mod configuration {
- use rome_service::configuration::Configuration;
- use std::env::current_dir;
- use std::fs::read_to_string;
-
- #[test]
- fn parse_all_fields() {
- let mut working_dir = current_dir().unwrap();
- working_dir.push("tests");
- working_dir.push("all_fields.json");
- let content = read_to_string(working_dir).unwrap();
-
- let configuration = serde_json::from_str::<Configuration>(&content);
-
- assert!(configuration.is_ok())
- }
-
- #[test]
- fn parse_default_values() {
- let mut working_dir = current_dir().unwrap();
- working_dir.push("tests");
- working_dir.push("empty.json");
- let content = read_to_string(working_dir).unwrap();
-
- let configuration = serde_json::from_str::<Configuration>(&content);
-
- match configuration {
- Ok(configuration) => {
- assert_eq!(configuration, Configuration::default());
- }
- Err(err) => {
- panic!("{err}");
- }
- }
- }
-
- #[test]
- fn line_width_error() {
- let mut working_dir = current_dir().unwrap();
- working_dir.push("tests");
- working_dir.push("line_width.json");
- let content = read_to_string(working_dir).unwrap();
-
- let configuration = serde_json::from_str::<Configuration>(&content);
-
- assert!(configuration.is_err());
-
- if let Err(err) = configuration {
- assert!(err
- .to_string()
- .as_str()
- .contains("The line width exceeds the maximum value (320)"),)
- }
- }
-}

feat(rome_cli): read configuration file from CLI (#2851)