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
158 changes: 114 additions & 44 deletions apps/oxfmt/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ use cow_utils::CowUtils;
use rayon::prelude::*;

use oxc_allocator::AllocatorPool;
use oxc_diagnostics::{DiagnosticSender, DiagnosticService, OxcDiagnostic};
use oxc_formatter::{FormatOptions, Formatter, enable_jsx_source_type, get_parse_options};
use oxc_diagnostics::{DiagnosticSender, DiagnosticService, Error, OxcDiagnostic};
use oxc_formatter::{
FormatOptions, Formatter, enable_jsx_source_type, get_parse_options, get_supported_source_type,
};
use oxc_parser::Parser;
use oxc_span::SourceType;

use crate::{command::OutputOptions, walk::WalkEntry};

Expand Down Expand Up @@ -73,9 +76,51 @@ impl FormatService {
let start_time = Instant::now();

let path = &entry.path;
let source_type = enable_jsx_source_type(entry.source_type);
let (code, is_changed) = match self.format_file(entry) {
Ok(res) => res,
Err(diagnostics) => {
tx_error.send(diagnostics).unwrap();
return;
}
};

let elapsed = start_time.elapsed();

// Write back if needed
if matches!(self.output_options, OutputOptions::Write) && is_changed {
fs::write(path, code)
.map_err(|_| format!("Failed to write to '{}'", path.to_string_lossy()))
.unwrap();
}

// Notify if needed
if let Some(output) = match (&self.output_options, is_changed) {
(OutputOptions::Check | OutputOptions::ListDifferent, true) => {
let display_path = path
// Show path relative to `cwd` for cleaner output
.strip_prefix(&self.cwd)
.unwrap_or(path)
.to_string_lossy()
// Normalize path separators for consistent output across platforms
.cow_replace('\\', "/")
.to_string();
let elapsed = elapsed.as_millis();

if matches!(self.output_options, OutputOptions::Check) {
Some(format!("{display_path} ({elapsed}ms)"))
} else {
Some(display_path)
}
}
_ => None,
} {
tx_path.send(output).unwrap();
}
}

fn format_file(&self, entry: &WalkEntry) -> Result<(String, bool), Vec<Error>> {
let path = &entry.path;

let allocator = self.allocator_pool.get();
let Ok(source_text) = read_to_string(path) else {
// This happens if `.ts` for MPEG-TS binary is attempted to be formatted
let diagnostics = DiagnosticService::wrap_diagnostics(
Expand All @@ -87,22 +132,46 @@ impl FormatService {
.with_help("This may be due to the file being a binary or inaccessible."),
],
);
tx_error.send(diagnostics).unwrap();
return;
return Err(diagnostics);
};

let code = match get_supported_source_type(path.as_path()) {
Some(source_type) => self.format_by_oxc_formatter(entry, &source_text, source_type)?,
#[cfg(feature = "napi")]
None => self.format_by_external_formatter(entry, &source_text)?,
#[cfg(not(feature = "napi"))]
None => unreachable!(
"If `napi` feature is disabled, non-supported entry should not be passed: {}",
path.display()
),
};

let ret = Parser::new(&allocator, &source_text, source_type)
let is_changed = source_text != code;

Ok((code, is_changed))
}

fn format_by_oxc_formatter(
&self,
entry: &WalkEntry,
source_text: &str,
source_type: SourceType,
) -> Result<String, Vec<Error>> {
let path = &entry.path;
let source_type = enable_jsx_source_type(source_type);

let allocator = self.allocator_pool.get();
let ret = Parser::new(&allocator, source_text, source_type)
.with_options(get_parse_options())
.parse();
if !ret.errors.is_empty() {
let diagnostics = DiagnosticService::wrap_diagnostics(
self.cwd.clone(),
path,
&source_text,
source_text,
ret.errors,
);
tx_error.send(diagnostics).unwrap();
return;
return Err(diagnostics);
}

let base_formatter = Formatter::new(&allocator, self.format_options.clone());
Expand All @@ -129,58 +198,59 @@ impl FormatService {
let diagnostics = DiagnosticService::wrap_diagnostics(
self.cwd.clone(),
path,
&source_text,
source_text,
vec![OxcDiagnostic::error(format!(
"Failed to print formatted code: {}\n{err}",
path.display()
))],
);
tx_error.send(diagnostics).unwrap();
return;
return Err(diagnostics);
}
};

#[cfg(feature = "detect_code_removal")]
{
if let Some(diff) = oxc_formatter::detect_code_removal(&source_text, &code, source_type)
if let Some(diff) = oxc_formatter::detect_code_removal(source_text, &code, source_type)
{
unreachable!("Code removal detected in `{}`:\n{diff}", path.to_string_lossy());
}
}

let elapsed = start_time.elapsed();
let is_changed = source_text != code;

// Write back if needed
if matches!(self.output_options, OutputOptions::Write) && is_changed {
fs::write(path, code)
.map_err(|_| format!("Failed to write to '{}'", path.to_string_lossy()))
.unwrap();
}
Ok(code)
}

// Notify if needed
if let Some(output) = match (&self.output_options, is_changed) {
(OutputOptions::Check | OutputOptions::ListDifferent, true) => {
let display_path = path
// Show path relative to `cwd` for cleaner output
.strip_prefix(&self.cwd)
.unwrap_or(path)
.to_string_lossy()
// Normalize path separators for consistent output across platforms
.cow_replace('\\', "/")
.to_string();
let elapsed = elapsed.as_millis();
#[cfg(feature = "napi")]
fn format_by_external_formatter(
&self,
entry: &WalkEntry,
source_text: &str,
) -> Result<String, Vec<Error>> {
let path = &entry.path;
let file_name = path.file_name().expect("Path must have a file name").to_string_lossy();

if matches!(self.output_options, OutputOptions::Check) {
Some(format!("{display_path} ({elapsed}ms)"))
} else {
Some(display_path)
}
let code = match self
.external_formatter
.as_ref()
.expect("`external_formatter` must exist when `napi` feature is enabled")
.format_file(&file_name, source_text)
{
Ok(code) => code,
Err(err) => {
// TODO: Need to handle `UndefinedParserError` or not
let diagnostics = DiagnosticService::wrap_diagnostics(
self.cwd.clone(),
path,
source_text,
vec![OxcDiagnostic::error(format!(
"Failed to format file with external formatter: {}\n{err}",
path.display()
))],
);
return Err(diagnostics);
}
_ => None,
} {
tx_path.send(output).unwrap();
}
};

Ok(code)
}
}

Expand Down
7 changes: 3 additions & 4 deletions apps/oxfmt/src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use std::{
use ignore::{gitignore::GitignoreBuilder, overrides::OverrideBuilder};

use oxc_formatter::get_supported_source_type;
use oxc_span::SourceType;

pub struct Walk {
inner: ignore::WalkParallel,
Expand Down Expand Up @@ -202,7 +201,6 @@ fn load_ignore_paths(cwd: &Path, ignore_paths: &[PathBuf]) -> Vec<PathBuf> {

pub struct WalkEntry {
pub path: PathBuf,
pub source_type: SourceType,
}

struct WalkBuilder {
Expand Down Expand Up @@ -230,9 +228,10 @@ impl ignore::ParallelVisitor for WalkVisitor {
// Use `is_file()` to detect symlinks to the directory named `.js`
#[expect(clippy::filetype_is_file)]
if file_type.is_file()
&& let Some(source_type) = get_supported_source_type(entry.path())
// TODO: Remove it for napi, keep it for not(napi)
&& get_supported_source_type(entry.path()).is_some()
{
let walk_entry = WalkEntry { path: entry.path().to_path_buf(), source_type };
let walk_entry = WalkEntry { path: entry.path().to_path_buf() };
// Send each entry immediately through the channel
// If send fails, the receiver has been dropped, so stop walking
if self.sender.send(walk_entry).is_err() {
Expand Down
Loading