diff --git a/Cargo.lock b/Cargo.lock index eb38ce443e9d5..4d8fd878d56b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1944,7 +1944,6 @@ version = "0.18.0" dependencies = [ "cow-utils", "insta", - "json-strip-comments", "natord", "oxc-schemars", "oxc_allocator", @@ -1999,6 +1998,7 @@ dependencies = [ "futures", "ignore", "insta", + "json-strip-comments", "log", "oxc_allocator", "oxc_data_structures", diff --git a/apps/oxfmt/src/core/config.rs b/apps/oxfmt/src/core/config.rs index a795f44af95d7..b12696c50e77b 100644 --- a/apps/oxfmt/src/core/config.rs +++ b/apps/oxfmt/src/core/config.rs @@ -2,7 +2,10 @@ use std::path::{Path, PathBuf}; use serde_json::Value; -use oxc_formatter::{FormatOptions, OxfmtOptions, Oxfmtrc}; +use oxc_formatter::{ + FormatOptions, + oxfmtrc::{OxfmtOptions, Oxfmtrc}, +}; use super::utils; diff --git a/crates/oxc_formatter/Cargo.toml b/crates/oxc_formatter/Cargo.toml index ae0769be4e2d2..9973abcaa18c6 100644 --- a/crates/oxc_formatter/Cargo.toml +++ b/crates/oxc_formatter/Cargo.toml @@ -29,7 +29,6 @@ oxc_span = { workspace = true } oxc_syntax = { workspace = true } cow-utils = { workspace = true } -json-strip-comments = { workspace = true } natord = "1.0.9" phf = { workspace = true, features = ["macros"] } rustc-hash = { workspace = true } diff --git a/crates/oxc_formatter/src/lib.rs b/crates/oxc_formatter/src/lib.rs index 40f45444c3136..a80a35b844947 100644 --- a/crates/oxc_formatter/src/lib.rs +++ b/crates/oxc_formatter/src/lib.rs @@ -7,6 +7,7 @@ mod embedded_formatter; mod formatter; mod ir_transform; mod options; +pub mod oxfmtrc; mod parentheses; mod service; mod utils; @@ -18,7 +19,7 @@ use oxc_ast::ast::*; pub use crate::embedded_formatter::{EmbeddedFormatter, EmbeddedFormatterCallback}; pub use crate::ir_transform::options::*; pub use crate::options::*; -pub use crate::service::{oxfmtrc::OxfmtOptions, oxfmtrc::Oxfmtrc, parse_utils::*}; +pub use crate::service::*; use crate::{ ast_nodes::{AstNode, AstNodes}, formatter::{FormatContext, Formatted}, diff --git a/crates/oxc_formatter/src/service/oxfmtrc.rs b/crates/oxc_formatter/src/oxfmtrc.rs similarity index 88% rename from crates/oxc_formatter/src/service/oxfmtrc.rs rename to crates/oxc_formatter/src/oxfmtrc.rs index 1f0c2b2849429..bddd07984bcc5 100644 --- a/crates/oxc_formatter/src/service/oxfmtrc.rs +++ b/crates/oxc_formatter/src/oxfmtrc.rs @@ -1,4 +1,9 @@ -use std::path::Path; +//! This file handles the `Oxfmtrc` struct, which ideally should be defined under `apps/oxfmt`. +//! +//! The reason it is not done so at this time is that `oxc_language_server` uses this struct, +//! and `apps/oxfmt` also depends on `oxc_language_server`, creating a circular reference. +//! +//! While it is possible to define a separate crate for `Oxfmtrc`, we compromise with this method for now. use schemars::{JsonSchema, schema_for}; use serde::{Deserialize, Deserializer, Serialize}; @@ -8,14 +13,16 @@ use crate::{ ArrowParentheses, AttributePosition, BracketSameLine, BracketSpacing, EmbeddedLanguageFormatting, Expand, FormatOptions, IndentStyle, IndentWidth, LineEnding, LineWidth, QuoteProperties, QuoteStyle, Semicolons, SortImportsOptions, SortOrder, - TrailingCommas, default_groups, default_internal_patterns, + TrailingCommas, }; /// Configuration options for the Oxfmt. +/// /// Most options are the same as Prettier's options. /// See also -/// But some options are our own extensions. -// All fields are typed as `Option` to distinguish between user-specified values and defaults. +/// +/// In addition, some options are our own extensions. +// NOTE: All fields are typed as `Option` to distinguish between user-specified values and defaults. #[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", default)] pub struct Oxfmtrc { @@ -68,10 +75,10 @@ pub struct Oxfmtrc { // Just be here to report error if they are used. #[serde(skip_serializing_if = "Option::is_none")] #[schemars(skip)] - pub experimental_operator_position: Option, + pub experimental_operator_position: Option, #[serde(skip_serializing_if = "Option::is_none")] #[schemars(skip)] - pub experimental_ternaries: Option, + pub experimental_ternaries: Option, /// Control whether formats quoted code embedded in the file. (Default: `"auto"`) #[serde(skip_serializing_if = "Option::is_none")] @@ -92,74 +99,74 @@ pub struct Oxfmtrc { // --- -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum EndOfLineConfig { - #[default] Lf, Crlf, Cr, } -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "kebab-case")] pub enum QuotePropsConfig { - #[default] AsNeeded, Consistent, Preserve, } -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum TrailingCommaConfig { - #[default] All, Es5, None, } -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum ArrowParensConfig { - #[default] Always, Avoid, } -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum ObjectWrapConfig { - #[default] Preserve, Collapse, Always, } -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum EmbeddedLanguageFormattingConfig { Auto, - // Disable by default at alpha release, synced with `options.rs` - #[default] Off, } #[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase", default)] pub struct SortImportsConfig { - #[serde(default)] - pub partition_by_newline: bool, - #[serde(default)] - pub partition_by_comment: bool, - #[serde(default)] - pub sort_side_effects: bool, + /// Partition imports by newlines. (Default: `false`) + #[serde(skip_serializing_if = "Option::is_none")] + pub partition_by_newline: Option, + /// Partition imports by comments. (Default: `false`) + #[serde(skip_serializing_if = "Option::is_none")] + pub partition_by_comment: Option, + /// Sort side-effect imports. (Default: `false`) + #[serde(skip_serializing_if = "Option::is_none")] + pub sort_side_effects: Option, + /// Sort order. (Default: `"asc"`) #[serde(skip_serializing_if = "Option::is_none")] pub order: Option, - #[serde(default = "default_true")] - pub ignore_case: bool, - #[serde(default = "default_true")] - pub newlines_between: bool, + /// Ignore case when sorting. (Default: `true`) + #[serde(skip_serializing_if = "Option::is_none")] + pub ignore_case: Option, + /// Add newlines between import groups. (Default: `true`) + #[serde(skip_serializing_if = "Option::is_none")] + pub newlines_between: Option, + /// Glob patterns to identify internal imports. #[serde(skip_serializing_if = "Option::is_none")] pub internal_pattern: Option>, /// Custom groups configuration for organizing imports. @@ -169,10 +176,6 @@ pub struct SortImportsConfig { pub groups: Option>>, } -fn default_true() -> bool { - true -} - /// Custom deserializer for groups field to support both `string` and `string[]` as group elements fn deserialize_groups<'de, D>(deserializer: D) -> Result>>, D::Error> where @@ -220,10 +223,9 @@ where } } -#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum SortOrderConfig { - #[default] Asc, Desc, } @@ -248,27 +250,9 @@ impl Default for OxfmtOptions { // --- 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 { - 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()))?; - - // JSONC support - strip comments - json_strip_comments::strip(&mut string) - .map_err(|err| format!("Failed to strip comments from {}: {err}", path.display()))?; - - // NOTE: String enum deserialization errors are handled here - serde_json::from_str(&string) - .map_err(|err| format!("Failed to deserialize config {}: {err}", path.display())) - } - + /// Converts the `Oxfmtrc` into `FormatOptions` and `OxfmtOptions`. + /// With resolving default values for unspecified options. + /// /// # Errors /// Returns error if any option value is invalid pub fn into_options(self) -> Result<(FormatOptions, OxfmtOptions), String> { @@ -282,6 +266,7 @@ impl Oxfmtrc { return Err("Unsupported option: `experimentalTernaries`".to_string()); } + // All values are based on defaults from `FormatOptions::default()` let mut format_options = FormatOptions::default(); // [Prettier] useTabs: boolean @@ -395,33 +380,52 @@ impl Oxfmtrc { // Below are our own extensions - if let Some(sort_imports_config) = self.experimental_sort_imports { - // `partition_by_newline: true` and `newlines_between` cannot be used together - if sort_imports_config.partition_by_newline && sort_imports_config.newlines_between { - return Err("Invalid `sortImports` configuration: `partitionByNewline: true` and `newlinesBetween: true` cannot be used together".to_string()); - } + if let Some(config) = self.experimental_sort_imports { + let mut sort_imports = SortImportsOptions::default(); - format_options.experimental_sort_imports = Some(SortImportsOptions { - partition_by_newline: sort_imports_config.partition_by_newline, - partition_by_comment: sort_imports_config.partition_by_comment, - sort_side_effects: sort_imports_config.sort_side_effects, - order: sort_imports_config.order.map_or(SortOrder::default(), |o| match o { + if let Some(v) = config.partition_by_newline { + sort_imports.partition_by_newline = v; + } + if let Some(v) = config.partition_by_comment { + sort_imports.partition_by_comment = v; + } + if let Some(v) = config.sort_side_effects { + sort_imports.sort_side_effects = v; + } + if let Some(v) = config.order { + sort_imports.order = match v { SortOrderConfig::Asc => SortOrder::Asc, SortOrderConfig::Desc => SortOrder::Desc, - }), - ignore_case: sort_imports_config.ignore_case, - newlines_between: sort_imports_config.newlines_between, - internal_pattern: sort_imports_config - .internal_pattern - .unwrap_or_else(default_internal_patterns), - groups: sort_imports_config.groups.unwrap_or_else(default_groups), - }); + }; + } + if let Some(v) = config.ignore_case { + sort_imports.ignore_case = v; + } + if let Some(v) = config.newlines_between { + sort_imports.newlines_between = v; + } + if let Some(v) = config.internal_pattern { + sort_imports.internal_pattern = v; + } + if let Some(v) = config.groups { + sort_imports.groups = v; + } + + // `partition_by_newline: true` and `newlines_between: true` cannot be used together + if sort_imports.partition_by_newline && sort_imports.newlines_between { + return Err("Invalid `sortImports` configuration: `partitionByNewline: true` and `newlinesBetween: true` cannot be used together".to_string()); + } + + format_options.experimental_sort_imports = Some(sort_imports); } - let oxfmt_options = OxfmtOptions { - ignore_patterns: self.ignore_patterns.unwrap_or_default(), - sort_package_json: self.experimental_sort_package_json.unwrap_or(true), - }; + let mut oxfmt_options = OxfmtOptions::default(); + if let Some(patterns) = self.ignore_patterns { + oxfmt_options.ignore_patterns = patterns; + } + if let Some(sort_package_json) = self.experimental_sort_package_json { + oxfmt_options.sort_package_json = sort_package_json; + } Ok((format_options, oxfmt_options)) } diff --git a/crates/oxc_formatter/src/service/mod.rs b/crates/oxc_formatter/src/service/mod.rs index d481da6c02fbf..2ade92da4fccd 100644 --- a/crates/oxc_formatter/src/service/mod.rs +++ b/crates/oxc_formatter/src/service/mod.rs @@ -1,2 +1,102 @@ -pub mod oxfmtrc; -pub mod parse_utils; +use oxc_parser::ParseOptions; +use oxc_span::SourceType; +use phf::phf_set; + +pub fn get_parse_options() -> ParseOptions { + ParseOptions { + // Do not need to parse regexp + parse_regular_expression: false, + // Enable all syntax features + allow_return_outside_function: true, + allow_v8_intrinsics: true, + // `oxc_formatter` expects this to be `false`, otherwise panics + preserve_parens: false, + } +} + +// Additional extensions from linguist-languages, which Prettier also supports +// - https://github.com/ikatyang-collab/linguist-languages/blob/d1dc347c7ced0f5b42dd66c7d1c4274f64a3eb6b/data/JavaScript.js +// No special extensions for TypeScript +// - https://github.com/ikatyang-collab/linguist-languages/blob/d1dc347c7ced0f5b42dd66c7d1c4274f64a3eb6b/data/TypeScript.js +// And on top of this data, Prettier adds its own checks. +// Ultimately, it can be confirmed with the following command. +// `prettier --support-info | jq '.languages[] | select(.name == "JavaScript")'` +static ADDITIONAL_JS_EXTENSIONS: phf::Set<&'static str> = phf_set! { + "_js", + "bones", + "es", + "es6", + "gs", + "jake", + "javascript", + "jsb", + "jscad", + "jsfl", + "jslib", + "jsm", + "jspre", + "jss", + "njs", + "pac", + "sjs", + "ssjs", + "xsjs", + "xsjslib", +}; + +// Special filenames that are valid JS files +static SPECIAL_JS_FILENAMES: phf::Set<&'static str> = phf_set! { + "Jakefile", + "start.frag", + "end.frag", +}; + +pub fn get_supported_source_type(path: &std::path::Path) -> Option { + // Standard extensions, also supported by `oxc_span::VALID_EXTENSIONS` + // NOTE: Use `path` directly for `.d.ts` detection + if let Ok(source_type) = SourceType::from_path(path) { + return Some(source_type); + } + + // Check special filenames first + if let Some(file_name) = path.file_name() + && SPECIAL_JS_FILENAMES.contains(file_name.to_str()?) + { + return Some(SourceType::default()); + } + + let extension = path.extension()?.to_string_lossy(); + // Additional extensions Prettier also supports + if ADDITIONAL_JS_EXTENSIONS.contains(extension.as_ref()) { + return Some(SourceType::default()); + } + // Special handling for `.frag` files: only allow `*.start.frag` and `*.end.frag` + if extension == "frag" { + let stem = path.file_stem()?.to_str()?; + #[expect(clippy::case_sensitive_file_extension_comparisons)] + return (stem.ends_with(".start") || stem.ends_with(".end")) + .then_some(SourceType::default()); + } + + None +} + +#[must_use] +pub fn enable_jsx_source_type(source_type: SourceType) -> SourceType { + if source_type.is_jsx() { + return source_type; + } + + // Always enable JSX for JavaScript files, no syntax conflict + if source_type.is_javascript() { + return source_type.with_jsx(true); + } + + // Prettier uses `regexp.test(source_text)` to detect JSX in TypeScript files. + // But we don't follow it for now, since it hurts the performance. + // if source_type.is_typescript() { + // // See https://github.com/prettier/prettier/blob/0d1e7abd5037a1fe8fbcf88a4d8cd13ec4d13a78/src/language-js/parse/utils/jsx-regexp.evaluate.js + // } + + source_type +} diff --git a/crates/oxc_formatter/src/service/parse_utils.rs b/crates/oxc_formatter/src/service/parse_utils.rs deleted file mode 100644 index 2ade92da4fccd..0000000000000 --- a/crates/oxc_formatter/src/service/parse_utils.rs +++ /dev/null @@ -1,102 +0,0 @@ -use oxc_parser::ParseOptions; -use oxc_span::SourceType; -use phf::phf_set; - -pub fn get_parse_options() -> ParseOptions { - ParseOptions { - // Do not need to parse regexp - parse_regular_expression: false, - // Enable all syntax features - allow_return_outside_function: true, - allow_v8_intrinsics: true, - // `oxc_formatter` expects this to be `false`, otherwise panics - preserve_parens: false, - } -} - -// Additional extensions from linguist-languages, which Prettier also supports -// - https://github.com/ikatyang-collab/linguist-languages/blob/d1dc347c7ced0f5b42dd66c7d1c4274f64a3eb6b/data/JavaScript.js -// No special extensions for TypeScript -// - https://github.com/ikatyang-collab/linguist-languages/blob/d1dc347c7ced0f5b42dd66c7d1c4274f64a3eb6b/data/TypeScript.js -// And on top of this data, Prettier adds its own checks. -// Ultimately, it can be confirmed with the following command. -// `prettier --support-info | jq '.languages[] | select(.name == "JavaScript")'` -static ADDITIONAL_JS_EXTENSIONS: phf::Set<&'static str> = phf_set! { - "_js", - "bones", - "es", - "es6", - "gs", - "jake", - "javascript", - "jsb", - "jscad", - "jsfl", - "jslib", - "jsm", - "jspre", - "jss", - "njs", - "pac", - "sjs", - "ssjs", - "xsjs", - "xsjslib", -}; - -// Special filenames that are valid JS files -static SPECIAL_JS_FILENAMES: phf::Set<&'static str> = phf_set! { - "Jakefile", - "start.frag", - "end.frag", -}; - -pub fn get_supported_source_type(path: &std::path::Path) -> Option { - // Standard extensions, also supported by `oxc_span::VALID_EXTENSIONS` - // NOTE: Use `path` directly for `.d.ts` detection - if let Ok(source_type) = SourceType::from_path(path) { - return Some(source_type); - } - - // Check special filenames first - if let Some(file_name) = path.file_name() - && SPECIAL_JS_FILENAMES.contains(file_name.to_str()?) - { - return Some(SourceType::default()); - } - - let extension = path.extension()?.to_string_lossy(); - // Additional extensions Prettier also supports - if ADDITIONAL_JS_EXTENSIONS.contains(extension.as_ref()) { - return Some(SourceType::default()); - } - // Special handling for `.frag` files: only allow `*.start.frag` and `*.end.frag` - if extension == "frag" { - let stem = path.file_stem()?.to_str()?; - #[expect(clippy::case_sensitive_file_extension_comparisons)] - return (stem.ends_with(".start") || stem.ends_with(".end")) - .then_some(SourceType::default()); - } - - None -} - -#[must_use] -pub fn enable_jsx_source_type(source_type: SourceType) -> SourceType { - if source_type.is_jsx() { - return source_type; - } - - // Always enable JSX for JavaScript files, no syntax conflict - if source_type.is_javascript() { - return source_type.with_jsx(true); - } - - // Prettier uses `regexp.test(source_text)` to detect JSX in TypeScript files. - // But we don't follow it for now, since it hurts the performance. - // if source_type.is_typescript() { - // // See https://github.com/prettier/prettier/blob/0d1e7abd5037a1fe8fbcf88a4d8cd13ec4d13a78/src/language-js/parse/utils/jsx-regexp.evaluate.js - // } - - source_type -} diff --git a/crates/oxc_formatter/tests/ir_transform/mod.rs b/crates/oxc_formatter/tests/ir_transform/mod.rs index d1b189464943b..7bd7b144d1289 100644 --- a/crates/oxc_formatter/tests/ir_transform/mod.rs +++ b/crates/oxc_formatter/tests/ir_transform/mod.rs @@ -1,6 +1,6 @@ mod sort_imports; -use oxc_formatter::{FormatOptions, Oxfmtrc}; +use oxc_formatter::{FormatOptions, oxfmtrc::Oxfmtrc}; pub fn assert_format(code: &str, config_json: &str, expected: &str) { // NOTE: Strip leading single `\n` for better test case readability. diff --git a/crates/oxc_formatter/tests/schema.rs b/crates/oxc_formatter/tests/schema.rs index af6ec02a21412..88cede9316b34 100644 --- a/crates/oxc_formatter/tests/schema.rs +++ b/crates/oxc_formatter/tests/schema.rs @@ -1,6 +1,6 @@ use std::fs; -use oxc_formatter::Oxfmtrc; +use oxc_formatter::oxfmtrc::Oxfmtrc; use project_root::get_project_root; // NOTE: This test generates the JSON schema for the `.oxfmtrc.json` configuration file diff --git a/crates/oxc_formatter/tests/snapshots/schema_json.snap b/crates/oxc_formatter/tests/snapshots/schema_json.snap index d53bc24ce16db..8de27c64e9ba7 100644 --- a/crates/oxc_formatter/tests/snapshots/schema_json.snap +++ b/crates/oxc_formatter/tests/snapshots/schema_json.snap @@ -62,21 +62,31 @@ expression: json ] }, "ignoreCase": { - "default": true, - "type": "boolean" + "description": "Ignore case when sorting. (Default: `true`)", + "markdownDescription": "Ignore case when sorting. (Default: `true`)", + "type": [ + "boolean", + "null" + ] }, "internalPattern": { + "description": "Glob patterns to identify internal imports.", "items": { "type": "string" }, + "markdownDescription": "Glob patterns to identify internal imports.", "type": [ "array", "null" ] }, "newlinesBetween": { - "default": true, - "type": "boolean" + "description": "Add newlines between import groups. (Default: `true`)", + "markdownDescription": "Add newlines between import groups. (Default: `true`)", + "type": [ + "boolean", + "null" + ] }, "order": { "anyOf": [ @@ -86,19 +96,33 @@ expression: json { "type": "null" } - ] + ], + "description": "Sort order. (Default: `\"asc\"`)", + "markdownDescription": "Sort order. (Default: `\"asc\"`)" }, "partitionByComment": { - "default": false, - "type": "boolean" + "description": "Partition imports by comments. (Default: `false`)", + "markdownDescription": "Partition imports by comments. (Default: `false`)", + "type": [ + "boolean", + "null" + ] }, "partitionByNewline": { - "default": false, - "type": "boolean" + "description": "Partition imports by newlines. (Default: `false`)", + "markdownDescription": "Partition imports by newlines. (Default: `false`)", + "type": [ + "boolean", + "null" + ] }, "sortSideEffects": { - "default": false, - "type": "boolean" + "description": "Sort side-effect imports. (Default: `false`)", + "markdownDescription": "Sort side-effect imports. (Default: `false`)", + "type": [ + "boolean", + "null" + ] } }, "type": "object" @@ -119,8 +143,8 @@ expression: json "type": "string" } }, - "description": "Configuration options for the Oxfmt.\nMost options are the same as Prettier's options.\nSee also \nBut some options are our own extensions.", - "markdownDescription": "Configuration options for the Oxfmt.\nMost options are the same as Prettier's options.\nSee also \nBut some options are our own extensions.", + "description": "Configuration options for the Oxfmt.\n\nMost options are the same as Prettier's options.\nSee also \n\nIn addition, some options are our own extensions.", + "markdownDescription": "Configuration options for the Oxfmt.\n\nMost options are the same as Prettier's options.\nSee also \n\nIn addition, some options are our own extensions.", "properties": { "arrowParens": { "anyOf": [ diff --git a/crates/oxc_language_server/Cargo.toml b/crates/oxc_language_server/Cargo.toml index 5a2340adaee1a..a2aaf45fd2672 100644 --- a/crates/oxc_language_server/Cargo.toml +++ b/crates/oxc_language_server/Cargo.toml @@ -33,6 +33,7 @@ oxc_parser = { workspace = true, optional = true } env_logger = { workspace = true, features = ["humantime"] } futures = { workspace = true } ignore = { workspace = true, features = ["simd-accel"], optional = true } +json-strip-comments = { workspace = true } log = { workspace = true } papaya = { workspace = true } rustc-hash = { workspace = true } diff --git a/crates/oxc_language_server/src/formatter/server_formatter.rs b/crates/oxc_language_server/src/formatter/server_formatter.rs index 89537e06040c1..81fe44e102fc8 100644 --- a/crates/oxc_language_server/src/formatter/server_formatter.rs +++ b/crates/oxc_language_server/src/formatter/server_formatter.rs @@ -5,8 +5,8 @@ use log::{debug, warn}; use oxc_allocator::Allocator; use oxc_data_structures::rope::{Rope, get_line_column}; use oxc_formatter::{ - FormatOptions, Formatter, OxfmtOptions, Oxfmtrc, enable_jsx_source_type, get_parse_options, - get_supported_source_type, + FormatOptions, Formatter, enable_jsx_source_type, get_parse_options, get_supported_source_type, + oxfmtrc::{OxfmtOptions, Oxfmtrc}, }; use oxc_parser::Parser; use tower_lsp_server::ls_types::{Pattern, Position, Range, ServerCapabilities, TextEdit, Uri}; @@ -70,7 +70,7 @@ impl ToolBuilder for ServerFormatterBuilder { impl ServerFormatterBuilder { fn get_config(root_path: &Path, config_path: Option<&String>) -> Oxfmtrc { if let Some(config) = Self::search_config_file(root_path, config_path) { - if let Ok(oxfmtrc) = Oxfmtrc::from_file(&config) { + if let Ok(oxfmtrc) = Self::from_file(&config) { oxfmtrc } else { warn!("Failed to initialize oxfmtrc config: {}", config.to_string_lossy()); @@ -84,6 +84,26 @@ impl ServerFormatterBuilder { Oxfmtrc::default() } } + + /// # Errors + /// Returns error if: + /// - file cannot be found or read + /// - file content is not valid JSONC + /// - deserialization fails for string enum values + fn from_file(path: &Path) -> Result { + 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()))?; + + // JSONC support - strip comments + json_strip_comments::strip(&mut string) + .map_err(|err| format!("Failed to strip comments from {}: {err}", path.display()))?; + + // NOTE: String enum deserialization errors are handled here + serde_json::from_str(&string) + .map_err(|err| format!("Failed to deserialize config {}: {err}", path.display())) + } + fn get_options(oxfmtrc: Oxfmtrc) -> (FormatOptions, OxfmtOptions) { match oxfmtrc.into_options() { Ok(opts) => opts, diff --git a/npm/oxfmt/configuration_schema.json b/npm/oxfmt/configuration_schema.json index a8047728b15cf..244b5852f694d 100644 --- a/npm/oxfmt/configuration_schema.json +++ b/npm/oxfmt/configuration_schema.json @@ -58,21 +58,31 @@ ] }, "ignoreCase": { - "default": true, - "type": "boolean" + "description": "Ignore case when sorting. (Default: `true`)", + "markdownDescription": "Ignore case when sorting. (Default: `true`)", + "type": [ + "boolean", + "null" + ] }, "internalPattern": { + "description": "Glob patterns to identify internal imports.", "items": { "type": "string" }, + "markdownDescription": "Glob patterns to identify internal imports.", "type": [ "array", "null" ] }, "newlinesBetween": { - "default": true, - "type": "boolean" + "description": "Add newlines between import groups. (Default: `true`)", + "markdownDescription": "Add newlines between import groups. (Default: `true`)", + "type": [ + "boolean", + "null" + ] }, "order": { "anyOf": [ @@ -82,19 +92,33 @@ { "type": "null" } - ] + ], + "description": "Sort order. (Default: `\"asc\"`)", + "markdownDescription": "Sort order. (Default: `\"asc\"`)" }, "partitionByComment": { - "default": false, - "type": "boolean" + "description": "Partition imports by comments. (Default: `false`)", + "markdownDescription": "Partition imports by comments. (Default: `false`)", + "type": [ + "boolean", + "null" + ] }, "partitionByNewline": { - "default": false, - "type": "boolean" + "description": "Partition imports by newlines. (Default: `false`)", + "markdownDescription": "Partition imports by newlines. (Default: `false`)", + "type": [ + "boolean", + "null" + ] }, "sortSideEffects": { - "default": false, - "type": "boolean" + "description": "Sort side-effect imports. (Default: `false`)", + "markdownDescription": "Sort side-effect imports. (Default: `false`)", + "type": [ + "boolean", + "null" + ] } }, "type": "object" @@ -115,8 +139,8 @@ "type": "string" } }, - "description": "Configuration options for the Oxfmt.\nMost options are the same as Prettier's options.\nSee also \nBut some options are our own extensions.", - "markdownDescription": "Configuration options for the Oxfmt.\nMost options are the same as Prettier's options.\nSee also \nBut some options are our own extensions.", + "description": "Configuration options for the Oxfmt.\n\nMost options are the same as Prettier's options.\nSee also \n\nIn addition, some options are our own extensions.", + "markdownDescription": "Configuration options for the Oxfmt.\n\nMost options are the same as Prettier's options.\nSee also \n\nIn addition, some options are our own extensions.", "properties": { "arrowParens": { "anyOf": [ diff --git a/tasks/website_formatter/src/json_schema.rs b/tasks/website_formatter/src/json_schema.rs index 04e001d5ccbf5..52f54cc137627 100644 --- a/tasks/website_formatter/src/json_schema.rs +++ b/tasks/website_formatter/src/json_schema.rs @@ -1,4 +1,4 @@ -use oxc_formatter::Oxfmtrc; +use oxc_formatter::oxfmtrc::Oxfmtrc; use schemars::schema_for; use website_common::Renderer; diff --git a/tasks/website_formatter/src/snapshots/schema_markdown.snap b/tasks/website_formatter/src/snapshots/schema_markdown.snap index 81c6119045b2f..1db7e1e482a0c 100644 --- a/tasks/website_formatter/src/snapshots/schema_markdown.snap +++ b/tasks/website_formatter/src/snapshots/schema_markdown.snap @@ -7,9 +7,11 @@ search: false --- # Configuration options for the Oxfmt. + Most options are the same as Prettier's options. See also -But some options are our own extensions. + +In addition, some options are our own extensions. ## arrowParens @@ -81,11 +83,10 @@ type: `string[]` ### experimentalSortImports.ignoreCase -type: `boolean` - -default: `true` +type: `boolean | null` +Ignore case when sorting. (Default: `true`) ### experimentalSortImports.internalPattern @@ -93,16 +94,15 @@ default: `true` type: `string[]` - +Glob patterns to identify internal imports. ### experimentalSortImports.newlinesBetween -type: `boolean` - -default: `true` +type: `boolean | null` +Add newlines between import groups. (Default: `true`) ### experimentalSortImports.order @@ -110,34 +110,31 @@ default: `true` type: `string | null` - +Sort order. (Default: `"asc"`) ### experimentalSortImports.partitionByComment -type: `boolean` - -default: `false` +type: `boolean | null` +Partition imports by comments. (Default: `false`) ### experimentalSortImports.partitionByNewline -type: `boolean` - -default: `false` +type: `boolean | null` +Partition imports by newlines. (Default: `false`) ### experimentalSortImports.sortSideEffects -type: `boolean` - -default: `false` +type: `boolean | null` +Sort side-effect imports. (Default: `false`) ## experimentalSortPackageJson