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
7 changes: 7 additions & 0 deletions .changeset/keep-jsxeverywhere-variant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@biomejs/biome": patch
---

Fixed [#7286](https://github.com/biomejs/biome/issues/7286). Files are now formatted with JSX behavior when `javascript.parser.jsxEverywhere` is explicitly set.

Previously, this flag was only used for parsing, but not for formatting, which resulted in incorrect formatting of conditional expressions when JSX syntax is used in `.js` files.
19 changes: 2 additions & 17 deletions crates/biome_service/src/file_handlers/javascript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ use biome_js_parser::JsParserOptions;
use biome_js_semantic::{SemanticModelOptions, semantic_model};
use biome_js_syntax::{
AnyJsRoot, JsClassDeclaration, JsClassExpression, JsFileSource, JsFunctionDeclaration,
JsLanguage, JsSyntaxNode, JsVariableDeclarator, LanguageVariant, TextRange, TextSize,
TokenAtOffset,
JsLanguage, JsSyntaxNode, JsVariableDeclarator, TextRange, TextSize, TokenAtOffset,
};
use biome_js_type_info::{GlobalsResolver, ScopeId, TypeData, TypeResolver};
use biome_module_graph::ModuleGraph;
Expand Down Expand Up @@ -521,17 +520,7 @@ fn parse(
.override_settings
.apply_override_js_parser_options(biome_path, &mut options);

let mut file_source = file_source.to_js_file_source().unwrap_or_default();
let jsx_everywhere = settings
.languages
.javascript
.parser
.jsx_everywhere
.unwrap_or_default()
.into();
if jsx_everywhere && !file_source.is_typescript() {
file_source = file_source.with_variant(LanguageVariant::Jsx);
}
let file_source = file_source.to_js_file_source().unwrap_or_default();
let parse = biome_js_parser::parse_js_with_cache(text, file_source, options, cache);
ParseResult {
any_parse: parse.into(),
Expand Down Expand Up @@ -1094,7 +1083,3 @@ fn rename(
))
}
}

#[cfg(test)]
#[path = "javascript.tests.rs"]
mod tests;
41 changes: 0 additions & 41 deletions crates/biome_service/src/file_handlers/javascript.tests.rs

This file was deleted.

18 changes: 17 additions & 1 deletion crates/biome_service/src/workspace/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use biome_diagnostics::{
use biome_formatter::Printed;
use biome_fs::{BiomePath, ConfigName, PathKind};
use biome_grit_patterns::{CompilePatternOptions, GritQuery, compile_pattern_with_options};
use biome_js_syntax::{AnyJsRoot, ModuleKind};
use biome_js_syntax::{AnyJsRoot, LanguageVariant, ModuleKind};
use biome_json_parser::JsonParserOptions;
use biome_json_syntax::JsonFileSource;
use biome_module_graph::{ModuleDependencies, ModuleDiagnostic, ModuleGraph};
Expand Down Expand Up @@ -305,6 +305,22 @@ impl WorkspaceServer {
}
_ => {}
}
if !js.is_typescript() && !js.is_jsx() {
let settings = self
.projects
.get_settings_based_on_path(project_key, &biome_path)
.ok_or_else(WorkspaceError::no_project)?;
let jsx_everywhere = settings
.languages
.javascript
.parser
.jsx_everywhere
.unwrap_or_default()
.into();
if jsx_everywhere {
js.set_variant(LanguageVariant::Jsx);
}
}
}

let (content, version) = match content {
Expand Down
234 changes: 234 additions & 0 deletions crates/biome_service/src/workspace/server.tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
use biome_configuration::{
FormatterConfiguration, JsConfiguration,
javascript::{JsFormatterConfiguration, JsParserConfiguration},
};
use biome_formatter::{IndentStyle, LineWidth};
use biome_fs::MemoryFileSystem;
use biome_rowan::TextSize;

Expand Down Expand Up @@ -106,3 +111,232 @@ fn store_embedded_nodes_with_current_ranges() {
let style_node = style.node();
assert!(style_node.text_range_with_trivia().start() > TextSize::from(0));
}

#[test]
fn jsx_everywhere_sets_correct_variant() {
const TS_FILE_CONTENT: &[u8] = br"
const f = <T1>(arg1: T1) => <T2>(arg2: T2) => {
return { arg1, arg2 };
}
";
const JS_FILE_CONTENT: &[u8] = br"
function Foo({cond}) {
return cond ? (
<True />
) : (
<False />
);
}
";

let fs = MemoryFileSystem::default();
fs.insert(Utf8PathBuf::from("/project/a.ts"), TS_FILE_CONTENT);
fs.insert(Utf8PathBuf::from("/project/a.js"), JS_FILE_CONTENT);

let (workspace, project_key) = setup_workspace_and_open_project(fs, "/");

let js_conf = JsConfiguration {
parser: Some(JsParserConfiguration {
jsx_everywhere: Some(Bool(true)),
..Default::default()
}),
formatter: Some(JsFormatterConfiguration {
line_width: Some(LineWidth::try_from(30).unwrap()),
..Default::default()
}),
..Default::default()
};
let configuration = Configuration {
javascript: Some(js_conf),
formatter: Some(FormatterConfiguration {
indent_style: Some(IndentStyle::Space),
..Default::default()
}),
..Default::default()
};

workspace
.update_settings(UpdateSettingsParams {
project_key,
configuration,
workspace_directory: Some(BiomePath::new("/project")),
})
.unwrap();

workspace
.scan_project(ScanProjectParams {
project_key,
watch: false,
force: false,
scan_kind: ScanKind::Project,
verbose: false,
})
.unwrap();

workspace
.open_file(OpenFileParams {
project_key,
path: BiomePath::new("/project/a.ts"),
content: FileContent::FromServer,
document_file_source: None,
persist_node_cache: false,
})
.unwrap();

workspace
.open_file(OpenFileParams {
project_key,
path: BiomePath::new("/project/a.js"),
content: FileContent::FromServer,
document_file_source: None,
persist_node_cache: false,
})
.unwrap();

let ts_file_source = workspace.get_file_source("/project/a.ts".into());
let ts = ts_file_source.to_js_file_source().expect("JS file source");
assert!(ts.is_typescript());
assert!(!ts.is_jsx());
match workspace.get_parse("/project/a.ts".into()) {
Ok(parse) => assert_eq!(parse.diagnostics().len(), 0),
Err(error) => panic!("File not available: {error}"),
}

let js_file_source = workspace.get_file_source("/project/a.js".into());
let js = js_file_source.to_js_file_source().expect("JS file source");
assert!(!js.is_typescript());
assert!(js.is_jsx());
match workspace.get_parse("/project/a.js".into()) {
Ok(parse) => assert_eq!(parse.diagnostics().len(), 0),
Err(error) => panic!("File not available: {error}"),
}
match workspace.format_file(FormatFileParams {
project_key,
path: BiomePath::new("/project/a.js"),
}) {
Ok(printed) => {
insta::assert_snapshot!(printed.as_code(), @r###"
function Foo({ cond }) {
return cond ? (
<True />
) : (
<False />
);
}
"###);
}
Err(error) => panic!("File not formatted: {error}"),
}
}

#[test]
fn jsx_everywhere_disabled_correct_variant() {
const JS_FILE_CONTENT: &[u8] = br"
function Foo({cond}) {
return cond ? (
<True />
) : (
<False />
);
}
";

let fs = MemoryFileSystem::default();
fs.insert(Utf8PathBuf::from("/project/a.js"), JS_FILE_CONTENT);
fs.insert(Utf8PathBuf::from("/project/a.jsx"), JS_FILE_CONTENT);

let (workspace, project_key) = setup_workspace_and_open_project(fs, "/");

let js_conf = JsConfiguration {
parser: Some(JsParserConfiguration {
jsx_everywhere: Some(Bool(false)),
..Default::default()
}),
formatter: Some(JsFormatterConfiguration {
line_width: Some(LineWidth::try_from(30).unwrap()),
..Default::default()
}),
..Default::default()
};
let configuration = Configuration {
javascript: Some(js_conf),
formatter: Some(FormatterConfiguration {
indent_style: Some(IndentStyle::Space),
..Default::default()
}),
..Default::default()
};

workspace
.update_settings(UpdateSettingsParams {
project_key,
configuration,
workspace_directory: Some(BiomePath::new("/project")),
})
.unwrap();

workspace
.scan_project(ScanProjectParams {
project_key,
watch: false,
force: false,
scan_kind: ScanKind::Project,
verbose: false,
})
.unwrap();

workspace
.open_file(OpenFileParams {
project_key,
path: BiomePath::new("/project/a.js"),
content: FileContent::FromServer,
document_file_source: None,
persist_node_cache: false,
})
.unwrap();

workspace
.open_file(OpenFileParams {
project_key,
path: BiomePath::new("/project/a.jsx"),
content: FileContent::FromServer,
document_file_source: None,
persist_node_cache: false,
})
.unwrap();

let js_file_source = workspace.get_file_source("/project/a.js".into());
let js = js_file_source.to_js_file_source().expect("JS file source");
assert!(!js.is_typescript());
assert!(!js.is_jsx());
match workspace.get_parse("/project/a.js".into()) {
Ok(parse) => assert_ne!(parse.diagnostics().len(), 0),
Err(error) => panic!("File not available: {error}"),
}

let jsx_file_source = workspace.get_file_source("/project/a.jsx".into());
let jsx = jsx_file_source.to_js_file_source().expect("JS file source");
assert!(!jsx.is_typescript());
assert!(jsx.is_jsx());
match workspace.get_parse("/project/a.jsx".into()) {
Ok(parse) => assert_eq!(parse.diagnostics().len(), 0),
Err(error) => panic!("File not available: {error}"),
}
match workspace.format_file(FormatFileParams {
project_key,
path: BiomePath::new("/project/a.jsx"),
}) {
Ok(printed) => {
insta::assert_snapshot!(printed.as_code(), @r###"
function Foo({ cond }) {
return cond ? (
<True />
) : (
<False />
);
}
"###);
}
Err(error) => panic!("File not formatted: {error}"),
}
}