diff --git a/.changeset/keep-jsxeverywhere-variant.md b/.changeset/keep-jsxeverywhere-variant.md new file mode 100644 index 000000000000..b2201b56c44f --- /dev/null +++ b/.changeset/keep-jsxeverywhere-variant.md @@ -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. diff --git a/crates/biome_service/src/file_handlers/javascript.rs b/crates/biome_service/src/file_handlers/javascript.rs index fd4a4df953a3..7dbb8ff64456 100644 --- a/crates/biome_service/src/file_handlers/javascript.rs +++ b/crates/biome_service/src/file_handlers/javascript.rs @@ -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; @@ -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(), @@ -1094,7 +1083,3 @@ fn rename( )) } } - -#[cfg(test)] -#[path = "javascript.tests.rs"] -mod tests; diff --git a/crates/biome_service/src/file_handlers/javascript.tests.rs b/crates/biome_service/src/file_handlers/javascript.tests.rs deleted file mode 100644 index 7debf040b8c2..000000000000 --- a/crates/biome_service/src/file_handlers/javascript.tests.rs +++ /dev/null @@ -1,41 +0,0 @@ -use biome_configuration::bool::Bool; -use biome_configuration::javascript::JsParserConfiguration; -use biome_configuration::{Configuration, JsConfiguration}; -use biome_fs::BiomePath; - -use crate::settings::Settings; - -use super::*; - -#[test] -fn correctly_parses_ts_generics_with_jsx_everywhere() { - let js_conf = JsConfiguration { - parser: Some(JsParserConfiguration { - jsx_everywhere: Some(Bool(true)), - ..Default::default() - }), - ..Default::default() - }; - let configuration = Configuration { - javascript: Some(js_conf), - ..Default::default() - }; - let mut settings = Settings::default(); - settings - .merge_with_configuration(configuration, None) - .expect("valid configuration"); - - let source = r#" -const f = (arg1: T1) => (arg2: T2) => { - return { arg1, arg2 }; -} -"#; - let result = parse( - &BiomePath::new("file.test"), - DocumentFileSource::Js(JsFileSource::ts()), - source, - &settings, - &mut NodeCache::default(), - ); - assert!(!result.any_parse.has_errors()); -} diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 66bd3e8eccda..0a28a5d44a6a 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -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}; @@ -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 { diff --git a/crates/biome_service/src/workspace/server.tests.rs b/crates/biome_service/src/workspace/server.tests.rs index b10e71a3525f..1be74d379e9c 100644 --- a/crates/biome_service/src/workspace/server.tests.rs +++ b/crates/biome_service/src/workspace/server.tests.rs @@ -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; @@ -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 = (arg1: T1) => (arg2: T2) => { + return { arg1, arg2 }; +} + "; + const JS_FILE_CONTENT: &[u8] = br" +function Foo({cond}) { + return cond ? ( + + ) : ( + + ); +} + "; + + 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 ? ( + + ) : ( + + ); + } + "###); + } + 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 ? ( + + ) : ( + + ); +} + "; + + 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 ? ( + + ) : ( + + ); + } + "###); + } + Err(error) => panic!("File not formatted: {error}"), + } +}