diff --git a/crates/biome_json_syntax/src/file_source.rs b/crates/biome_json_syntax/src/file_source.rs index 6cb54d092a97..0c14c8fe6382 100644 --- a/crates/biome_json_syntax/src/file_source.rs +++ b/crates/biome_json_syntax/src/file_source.rs @@ -1,50 +1,23 @@ use biome_rowan::FileSourceError; use std::path::Path; -// TODO: Jsonc is not well-defined, so some files may only support comments -// but no trailing commas. We should reconsider whether to use Jsonc as a variant, -// or just use something like: JsonWithComments, JsonWithCommentsAndTrailingCommas -// -// Currently, the "variant" key and other properties are making things duplicated -// and error-prone. - #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[derive( Debug, Clone, Default, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, )] pub struct JsonFileSource { - variant: JsonVariant, allow_trailing_commas: bool, allow_comments: bool, } -#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] -#[derive( - Debug, Clone, Default, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, -)] -enum JsonVariant { - #[default] - Standard, - Jsonc, -} - impl JsonFileSource { pub fn json() -> Self { Self { - variant: JsonVariant::Standard, allow_trailing_commas: false, allow_comments: false, } } - pub fn jsonc() -> Self { - Self { - variant: JsonVariant::Jsonc, - allow_trailing_commas: true, - allow_comments: true, - } - } - pub fn with_trailing_commas(mut self, option_value: bool) -> Self { self.allow_trailing_commas = option_value; self @@ -70,10 +43,6 @@ impl JsonFileSource { pub fn get_allow_comments(&self) -> bool { self.allow_comments } - - pub const fn is_jsonc(&self) -> bool { - matches!(self.variant, JsonVariant::Jsonc) - } } impl TryFrom<&Path> for JsonFileSource { @@ -106,7 +75,7 @@ fn compute_source_type_from_path_or_extension( } else { match extension { "json" => JsonFileSource::json(), - "jsonc" => JsonFileSource::jsonc(), + "jsonc" => JsonFileSource::json().with_comments(true), _ => { return Err(FileSourceError::UnknownExtension( file_name.into(), diff --git a/crates/biome_service/src/diagnostics.rs b/crates/biome_service/src/diagnostics.rs index 3447875b70f6..96935840f714 100644 --- a/crates/biome_service/src/diagnostics.rs +++ b/crates/biome_service/src/diagnostics.rs @@ -519,8 +519,7 @@ impl Diagnostic for SourceFileNotSupported { } pub fn extension_error(path: &BiomePath) -> WorkspaceError { - let file_source = DocumentFileSource::from_path_and_known_filename(path) - .or(DocumentFileSource::from_path(path)); + let file_source = DocumentFileSource::from_path(path); WorkspaceError::source_file_not_supported( file_source, path.clone().display().to_string(), diff --git a/crates/biome_service/src/file_handlers/json.rs b/crates/biome_service/src/file_handlers/json.rs index 7ece6ab38911..e7bd448a304e 100644 --- a/crates/biome_service/src/file_handlers/json.rs +++ b/crates/biome_service/src/file_handlers/json.rs @@ -29,7 +29,7 @@ use biome_json_syntax::{JsonLanguage, JsonRoot, JsonSyntaxNode}; use biome_parser::AnyParse; use biome_rowan::{AstNode, NodeCache}; use biome_rowan::{TextRange, TextSize, TokenAtOffset}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] @@ -136,18 +136,6 @@ impl ExtensionHandler for JsonFileHandler { } } -fn is_file_allowed(path: &Path) -> bool { - path.file_name() - .and_then(|f| f.to_str()) - .map(|f| { - super::DocumentFileSource::WELL_KNOWN_JSONC_FILES - .binary_search(&f) - .is_ok() - }) - // default is false - .unwrap_or_default() -} - fn parse( biome_path: &BiomePath, file_source: DocumentFileSource, @@ -162,17 +150,11 @@ fn parse( biome_path, JsonParserOptions { allow_comments: parser.allow_comments - || optional_json_file_source.map_or(false, |x| x.get_allow_comments()) - || is_file_allowed(biome_path), + || optional_json_file_source.map_or(false, |x| x.get_allow_comments()), allow_trailing_commas: parser.allow_trailing_commas - || optional_json_file_source.map_or(false, |x| x.get_allow_trailing_commas()) - || is_file_allowed(biome_path), + || optional_json_file_source.map_or(false, |x| x.get_allow_trailing_commas()), }, ); - if let Some(mut json_file_source) = optional_json_file_source { - json_file_source.set_allow_trailing_commas(options.allow_trailing_commas); - json_file_source.set_allow_comments(options.allow_comments); - } let parse = biome_json_parser::parse_json_with_cache(text, cache, options); let root = parse.syntax(); let diagnostics = parse.into_diagnostics(); diff --git a/crates/biome_service/src/file_handlers/mod.rs b/crates/biome_service/src/file_handlers/mod.rs index ffdd56b453e2..351cb06cc823 100644 --- a/crates/biome_service/src/file_handlers/mod.rs +++ b/crates/biome_service/src/file_handlers/mod.rs @@ -76,13 +76,9 @@ impl From<&Path> for DocumentFileSource { } impl DocumentFileSource { - /// Sorted array of files that are known as JSONC (JSON with comments). - /// - /// TODO: We might want to narrow the list according to - /// https://github.com/prettier/prettier/issues/15945#issuecomment-1895371835 - /// - /// We can settle with these for now until someone raise a issue - pub(crate) const WELL_KNOWN_JSONC_FILES: &'static [&'static str; 15] = &[ + // Well known json-like files that support comments and trailing commas + // This list should be SORTED! + const WELL_KNOWN_JSON_WITH_COMMENTS_AND_TRAILING_COMMAS_FILES: &'static [&'static str] = &[ ".babelrc", ".babelrc.json", ".ember-cli", @@ -100,6 +96,10 @@ impl DocumentFileSource { "typescript.json", ]; + // Well known json-like files that support comments but no trailing commas + // This list should be SORTED! + const WELL_KNOWN_JSON_WITH_COMMENTS_FILES: &'static [&'static str] = &[]; + /// Returns the language corresponding to this file extension pub fn from_extension(s: &str) -> Self { match s.to_lowercase().as_str() { @@ -110,7 +110,7 @@ impl DocumentFileSource { "tsx" => JsFileSource::tsx().into(), "d.ts" | "d.mts" | "d.cts" => JsFileSource::d_ts().into(), "json" => JsonFileSource::json().into(), - "jsonc" => JsonFileSource::jsonc().into(), + "jsonc" => JsonFileSource::json().with_comments(true).into(), "astro" => JsFileSource::astro().into(), "vue" => JsFileSource::vue().into(), "svelte" => JsFileSource::svelte().into(), @@ -132,7 +132,7 @@ impl DocumentFileSource { "javascriptreact" => JsFileSource::jsx().into(), "typescriptreact" => JsFileSource::tsx().into(), "json" => JsonFileSource::json().into(), - "jsonc" => JsonFileSource::jsonc().into(), + "jsonc" => JsonFileSource::json().with_comments(true).into(), "astro" => JsFileSource::astro().into(), "vue" => JsFileSource::vue().into(), "svelte" => JsFileSource::svelte().into(), @@ -142,46 +142,51 @@ impl DocumentFileSource { } } - pub fn from_known_filename(s: &str) -> Self { - if Self::WELL_KNOWN_JSONC_FILES.binary_search(&s).is_ok() { - JsonFileSource::jsonc().into() - } else { - DocumentFileSource::Unknown - } - } - /// Returns the language corresponding to the file path pub fn from_path(path: &Path) -> Self { + // check well known files + if let Some(file_source) = path + .file_name() + .and_then(OsStr::to_str) + .and_then(DocumentFileSource::try_from_well_known_filename) + { + return file_source; + } + + // extract extensions let extension = match path { _ if path.to_str().is_some_and(|p| p.ends_with(".d.ts")) => Some("d.ts"), _ if path.to_str().is_some_and(|p| p.ends_with(".d.mts")) => Some("d.mts"), _ if path.to_str().is_some_and(|p| p.ends_with(".d.cts")) => Some("d.cts"), - path => path.extension().and_then(|path| path.to_str()), + path => path.extension().and_then(|e| e.to_str()), }; + // from extensions extension.map_or( DocumentFileSource::Unknown, DocumentFileSource::from_extension, ) } - /// Returns the language corresponding to the file path - /// relying on the file extension and the known files. - pub fn from_path_and_known_filename(path: &Path) -> Self { - let extension = match path { - _ if path.to_str().is_some_and(|p| p.ends_with(".d.ts")) => Some("d.ts"), - _ if path.to_str().is_some_and(|p| p.ends_with(".d.mts")) => Some("d.mts"), - _ if path.to_str().is_some_and(|p| p.ends_with(".d.cts")) => Some("d.cts"), - path => path.extension().and_then(|path| path.to_str()), - }; - - extension - .map(DocumentFileSource::from_extension) - .or(path - .file_name() - .and_then(OsStr::to_str) - .map(DocumentFileSource::from_known_filename)) - .unwrap_or_default() + fn try_from_well_known_filename(filename: &str) -> Option { + if Self::WELL_KNOWN_JSON_WITH_COMMENTS_AND_TRAILING_COMMAS_FILES + .binary_search(&filename) + .is_ok() + { + return Some( + JsonFileSource::json() + .with_comments(true) + .with_trailing_commas(true) + .into(), + ); + } + if Self::WELL_KNOWN_JSON_WITH_COMMENTS_FILES + .binary_search(&filename) + .is_ok() + { + return Some(JsonFileSource::json().with_comments(true).into()); + } + None } /// Returns the language if it's not unknown, otherwise returns `other`. @@ -288,7 +293,7 @@ impl biome_console::fmt::Display for DocumentFileSource { } } DocumentFileSource::Json(json) => { - if json.is_jsonc() { + if json.get_allow_comments() { fmt.write_markup(markup! { "JSONC" }) } else { fmt.write_markup(markup! { "JSON" }) @@ -509,7 +514,7 @@ impl Features { biome_path: &BiomePath, language_hint: DocumentFileSource, ) -> Capabilities { - match DocumentFileSource::from_path_and_known_filename(biome_path).or(language_hint) { + match DocumentFileSource::from_path(biome_path).or(language_hint) { DocumentFileSource::Js(source) => match source.as_embedding_kind() { EmbeddingKind::Astro => self.astro.capabilities(), EmbeddingKind::Vue => self.vue.capabilities(), @@ -554,7 +559,12 @@ pub(crate) fn is_diagnostic_error( #[test] fn test_order() { - for items in DocumentFileSource::WELL_KNOWN_JSONC_FILES.windows(2) { + for items in + DocumentFileSource::WELL_KNOWN_JSON_WITH_COMMENTS_AND_TRAILING_COMMAS_FILES.windows(2) + { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + for items in DocumentFileSource::WELL_KNOWN_JSON_WITH_COMMENTS_FILES.windows(2) { assert!(items[0] < items[1], "{} < {}", items[0], items[1]); } } diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 3a5d4f5347e9..d9ec42c1ecad 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -123,7 +123,7 @@ impl WorkspaceServer { move || { let file_source = self.get_file_source(path); - let language = DocumentFileSource::from_path_and_known_filename(path).or(file_source); + let language = DocumentFileSource::from_path(path).or(file_source); WorkspaceError::source_file_not_supported( language, path.clone().display().to_string(), @@ -322,7 +322,7 @@ impl Workspace for WorkspaceServer { } Entry::Vacant(entry) => { let capabilities = self.get_file_capabilities(¶ms.path); - let language = DocumentFileSource::from_path_and_known_filename(¶ms.path); + let language = DocumentFileSource::from_path(¶ms.path); let path = params.path.as_path(); let settings = self.settings.read().unwrap(); let mut file_features = FileFeaturesResult::new(); diff --git a/crates/biome_service/tests/workspace.rs b/crates/biome_service/tests/workspace.rs index a8cfe2bb3882..494d9803fa21 100644 --- a/crates/biome_service/tests/workspace.rs +++ b/crates/biome_service/tests/workspace.rs @@ -49,19 +49,86 @@ fn recognize_typescript_definition_file() { } #[test] -fn recognize_jsonc_file() { +fn correctly_handle_json_files() { let workspace = server(); - let file = FileGuard::open( + // ".json" file + let json_file = FileGuard::open( workspace.as_ref(), OpenFileParams { - path: BiomePath::new("a.jsonc"), - content: r#"{"a": 42,}//comment"#.into(), + path: BiomePath::new("a.json"), + content: r#"{"a": 42}"#.into(), version: 0, document_file_source: None, }, ) .unwrap(); + assert!(json_file.format_file().is_ok()); - assert!(file.format_file().is_ok()); + // ".json" file doesn't allow comments + let json_file_with_comments = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + path: BiomePath::new("b.json"), + content: r#"{"a": 42}//comment"#.into(), + version: 0, + document_file_source: None, + }, + ) + .unwrap(); + assert!(json_file_with_comments.format_file().is_err()); + + // ".json" file doesn't allow trailing commas + let json_file_with_trailing_commas = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + path: BiomePath::new("c.json"), + content: r#"{"a": 42,}"#.into(), + version: 0, + document_file_source: None, + }, + ) + .unwrap(); + assert!(json_file_with_trailing_commas.format_file().is_err()); + + // ".jsonc" file allows comments + let jsonc_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + path: BiomePath::new("d.jsonc"), + content: r#"{"a": 42}//comment"#.into(), + version: 0, + document_file_source: None, + }, + ) + .unwrap(); + assert!(jsonc_file.format_file().is_ok()); + + // ".jsonc" file doesn't allow trailing commas + let jsonc_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + path: BiomePath::new("e.jsonc"), + content: r#"{"a": 42,}"#.into(), + version: 0, + document_file_source: None, + }, + ) + .unwrap(); + assert!(jsonc_file.format_file().is_err()); + + // well-known json-with-comments-and-trailing-commas file allows comments and trailing commas + let well_known_json_with_comments_and_trailing_commas_file = FileGuard::open( + workspace.as_ref(), + OpenFileParams { + path: BiomePath::new("tsconfig.json"), + content: r#"{"a": 42,}//comment"#.into(), + version: 0, + document_file_source: None, + }, + ) + .unwrap(); + assert!(well_known_json_with_comments_and_trailing_commas_file + .format_file() + .is_ok()); } diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 1910557fff26..ab1ea4948eda 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1707,7 +1707,6 @@ export interface JsFileSource { export interface JsonFileSource { allow_comments: boolean; allow_trailing_commas: boolean; - variant: JsonVariant; } export interface CssFileSource { variant: CssVariant; @@ -1727,7 +1726,6 @@ export type LanguageVariant = "Standard" | "StandardRestricted" | "Jsx"; Defaults to the latest stable ECMAScript standard. */ export type LanguageVersion = "ES2022" | "ESNext"; -export type JsonVariant = "Standard" | "Jsonc"; /** * The style of CSS contained in the file. diff --git a/website/src/content/docs/guides/how-biome-works.mdx b/website/src/content/docs/guides/how-biome-works.mdx index 8bda26f79460..4cabab96f41b 100644 --- a/website/src/content/docs/guides/how-biome-works.mdx +++ b/website/src/content/docs/guides/how-biome-works.mdx @@ -166,13 +166,17 @@ The following files are currently ignored by Biome. This means that no diagnosti - `package-lock.json` - `yarn.lock` -The following files are parsed as **`JSON` files** with the options `json.parser.allowComments` and `json.parser.allowTrailingCommas` set to `true`. This is because editor tools like VSCode treat them like this. +## Well-known Files + +Here are some well-known files that we specifically treat based on their file names, rather than their extensions. Currently, the well-known files are JSON-like files only, but we may broaden the list to include other types when we support new parsers. + +The following files are parsed as **`JSON` files** with the options `json.parser.allowComments` and `json.parser.allowTrailingCommas` set to `true`. This is because the tools consuming these files are designed to accommodate such settings. -- `.babelrc.json` - `.babelrc` +- `.babelrc.json` - `.ember-cli` -- `.eslintrc.json` - `.eslintrc` +- `.eslintrc.json` - `.hintrc` - `.jsfmtrc` - `.jshintrc`