Skip to content

Commit

Permalink
refactor: json file source (#2150)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sec-ant authored Mar 21, 2024
1 parent 958a9a1 commit 4a34e6f
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 105 deletions.
33 changes: 1 addition & 32 deletions crates/biome_json_syntax/src/file_source.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
Expand Down
3 changes: 1 addition & 2 deletions crates/biome_service/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
24 changes: 3 additions & 21 deletions crates/biome_service/src/file_handlers/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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,
Expand All @@ -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();
Expand Down
86 changes: 48 additions & 38 deletions crates/biome_service/src/file_handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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() {
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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<DocumentFileSource> {
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`.
Expand Down Expand Up @@ -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" })
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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]);
}
}
4 changes: 2 additions & 2 deletions crates/biome_service/src/workspace/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -322,7 +322,7 @@ impl Workspace for WorkspaceServer {
}
Entry::Vacant(entry) => {
let capabilities = self.get_file_capabilities(&params.path);
let language = DocumentFileSource::from_path_and_known_filename(&params.path);
let language = DocumentFileSource::from_path(&params.path);
let path = params.path.as_path();
let settings = self.settings.read().unwrap();
let mut file_features = FileFeaturesResult::new();
Expand Down
77 changes: 72 additions & 5 deletions crates/biome_service/tests/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Loading

0 comments on commit 4a34e6f

Please sign in to comment.