Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions apps/oxfmt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ oxc_span = { workspace = true }
bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"] }
cow-utils = { workspace = true }
ignore = { workspace = true, features = ["simd-accel"] }
json-strip-comments = { workspace = true }
miette = { workspace = true }
phf = { workspace = true, features = ["macros"] }
rayon = { workspace = true }
serde_json = { workspace = true }
simdutf8 = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros"] }
tracing-subscriber = { workspace = true, features = [] } # Omit the `regex` feature
Expand Down
116 changes: 70 additions & 46 deletions apps/oxfmt/src/cli/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use std::{
};

use oxc_diagnostics::DiagnosticService;
use oxc_formatter::Oxfmtrc;
use oxc_formatter::{FormatOptions, Oxfmtrc};
use serde_json::{Map, Value};

use super::{
command::{FormatCommand, OutputOptions},
Expand All @@ -16,7 +17,7 @@ use super::{
service::{FormatService, SuccessResult},
walk::Walk,
};
use crate::core::SourceFormatter;
use crate::core::{SourceFormatter, utils};

#[derive(Debug)]
pub struct FormatRunner {
Expand Down Expand Up @@ -63,22 +64,18 @@ impl FormatRunner {
// NOTE: Currently, we only load single config file.
// - from `--config` if specified
// - else, search nearest for the nearest `.oxfmtrc.json` from cwd upwards
let config = match load_config(&cwd, basic_options.config.as_deref()) {
Ok(config) => config,
Err(err) => {
print_and_flush(stderr, &format!("Failed to load configuration file.\n{err}\n"));
return CliRunResult::InvalidOptionConfig;
}
};

let ignore_patterns = config.ignore_patterns.clone().unwrap_or_default();
let format_options = match config.into_format_options() {
Ok(options) => options,
Err(err) => {
print_and_flush(stderr, &format!("Failed to parse configuration.\n{err}\n"));
return CliRunResult::InvalidOptionConfig;
}
};
let config_path = load_config_path(&cwd, basic_options.config.as_deref());
let (format_options, ignore_patterns, raw_config) =
match load_config(config_path.as_deref()) {
Ok(c) => c,
Err(err) => {
print_and_flush(
stderr,
&format!("Failed to load configuration file.\n{err}\n"),
);
return CliRunResult::InvalidOptionConfig;
}
};

// TODO: Plugins support
// - Parse returned `languages`
Expand All @@ -89,15 +86,16 @@ impl FormatRunner {
.external_formatter
.as_ref()
.expect("External formatter must be set when `napi` feature is enabled")
// TODO: Construct actual config
.setup_config("{}")
.setup_config(&raw_config.to_string())
{
print_and_flush(
stderr,
&format!("Failed to setup external formatter config.\n{err}\n"),
);
return CliRunResult::InvalidOptionConfig;
}
#[cfg(not(feature = "napi"))]
let _ = raw_config;

let walker = match Walk::build(
&cwd,
Expand Down Expand Up @@ -233,38 +231,64 @@ impl FormatRunner {
}
}

/// # Errors
///
/// Returns error if:
/// - Config file is specified but not found or invalid
/// - Config file parsing fails
fn load_config(cwd: &Path, config_path: Option<&Path>) -> Result<Oxfmtrc, String> {
let config_path = if let Some(config_path) = config_path {
// If `--config` is explicitly specified, use that path
Some(if config_path.is_absolute() {
/// Resolve config file path from cwd and optional explicit path.
fn load_config_path(cwd: &Path, config_path: Option<&Path>) -> Option<PathBuf> {
// If `--config` is explicitly specified, use that path
if let Some(config_path) = config_path {
return Some(if config_path.is_absolute() {
config_path.to_path_buf()
} else {
cwd.join(config_path)
})
} else {
// If `--config` is not specified, search the nearest config file from cwd upwards
// Support both `.json` and `.jsonc`, but prefer `.json` if both exist
cwd.ancestors().find_map(|dir| {
for filename in [".oxfmtrc.json", ".oxfmtrc.jsonc"] {
let config_path = dir.join(filename);
if config_path.exists() {
return Some(config_path);
}
});
}

// If `--config` is not specified, search the nearest config file from cwd upwards
// Support both `.json` and `.jsonc`, but prefer `.json` if both exist
cwd.ancestors().find_map(|dir| {
for filename in [".oxfmtrc.json", ".oxfmtrc.jsonc"] {
let config_path = dir.join(filename);
if config_path.exists() {
return Some(config_path);
}
None
})
}
None
})
}

/// # Errors
/// Returns error if:
/// - Config file is specified but not found or invalid
/// - Config file parsing fails
fn load_config(config_path: Option<&Path>) -> Result<(FormatOptions, Vec<String>, Value), String> {
// Default if not specified and not found
let Some(path) = config_path else {
return Ok((FormatOptions::default(), vec![], Value::Object(Map::default())));
};

match config_path {
Some(ref path) => Oxfmtrc::from_file(path),
// Default if not specified and not found
None => Ok(Oxfmtrc::default()),
}
let mut json_string = utils::read_to_string(path)
// Do not include OS error, it differs between platforms
.map_err(|_| format!("Failed to read config {}: File not found", path.display()))?;
// Strip comments (JSONC support)
json_strip_comments::strip(&mut json_string)
.map_err(|err| format!("Failed to strip comments from {}: {err}", path.display()))?;

// Parse as raw JSON value (to pass to external formatter)
let raw_config: Value = serde_json::from_str(&json_string)
.map_err(|err| format!("Failed to parse config {}: {err}", path.display()))?;

// NOTE: Field validation for `enum` are done here
let oxfmtrc: Oxfmtrc = serde_json::from_str(&json_string)
.map_err(|err| format!("Failed to deserialize config {}: {err}", path.display()))?;

let ignore_patterns = oxfmtrc.ignore_patterns.clone().unwrap_or_default();
// NOTE: Other validation based on it's field values are done here
let format_options = oxfmtrc
.into_format_options()
.map_err(|err| format!("Failed to parse configuration.\n{err}"))?;

// TODO: Override `raw_config` with resolved options to apply our defaults

Ok((format_options, ignore_patterns, raw_config))
}

fn print_and_flush(writer: &mut dyn Write, message: &str) {
Expand Down
3 changes: 2 additions & 1 deletion crates/oxc_formatter/src/service/oxfmtrc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,14 @@ pub enum SortOrderConfig {
// ---

impl Oxfmtrc {
// TODO: Since `oxc_language_server/ServerFormatterBuilder` is the only user of this,
// use `Oxfmtrc` directly and remove.
/// # Errors
/// Returns error if:
/// - file cannot be found or read
/// - file content is not valid JSONC
/// - deserialization fails for string enum values
pub fn from_file(path: &Path) -> Result<Self, String> {
// TODO: Use `simdutf8` like `oxc_linter`?
let mut string = std::fs::read_to_string(path)
// Do not include OS error, it differs between platforms
.map_err(|_| format!("Failed to read config {}: File not found", path.display()))?;
Expand Down
Loading