diff --git a/.changeset/trailing-newline-option.md b/.changeset/trailing-newline-option.md new file mode 100644 index 000000000000..4631a6b48de3 --- /dev/null +++ b/.changeset/trailing-newline-option.md @@ -0,0 +1,31 @@ +--- +"@biomejs/biome": minor +--- + +Added the formatter option [`trailingNewline`](https://biomejs.dev/reference/configuration/#formattertrailingnewline). + +When set to `false`, the formatter will remove the trailing newline at the end of formatted files. The default value is `true`, which preserves the current behavior of adding a trailing newline. + +This option is available globally and for each language-specific formatter configuration: + +```json +{ + "formatter": { + "trailingNewline": false + }, + "javascript": { + "formatter": { + "trailingNewline": true + } + } +} +``` + +The following CLI flags have been added. They accept `true` or `false` as value: +- `--formatter-trailing-newline` +- `--javascript-formatter-trailing-newline` +- `--json-formatter-trailing-newline` +- `--graphql-formatter-trailing-newline` +- `--css-formatter-trailing-newline` +- `--html-formatter-trailing-newline` + diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000000..d51efb4aa9e7 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(cargo clippy:*)", + "Bash(cargo t:*)", + "Bash(cargo clean:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index 6a7970727add..a74b53dedbd0 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -310,16 +310,16 @@ pub enum BiomeCommand { #[bpaf(external(json_parser_configuration), optional, hide_usage)] json_parser: Option, - #[bpaf(external(css_parser_configuration), optional, hide_usage, hide)] + #[bpaf(external(css_parser_configuration), optional, hide_usage)] css_parser: Option, - #[bpaf(external(graphql_formatter_configuration), optional, hide_usage, hide)] + #[bpaf(external(graphql_formatter_configuration), optional, hide_usage)] graphql_formatter: Option, - #[bpaf(external(css_formatter_configuration), optional, hide_usage, hide)] + #[bpaf(external(css_formatter_configuration), optional, hide_usage)] css_formatter: Option, - #[bpaf(external(html_formatter_configuration), optional, hide_usage, hide)] + #[bpaf(external(html_formatter_configuration), optional, hide_usage)] html_formatter: Option, #[bpaf(external(vcs_configuration), optional, hide_usage)] diff --git a/crates/biome_cli/src/execute/migrate/prettier.rs b/crates/biome_cli/src/execute/migrate/prettier.rs index b36ecef3575e..e289338433f7 100644 --- a/crates/biome_cli/src/execute/migrate/prettier.rs +++ b/crates/biome_cli/src/execute/migrate/prettier.rs @@ -242,6 +242,7 @@ impl TryFrom for biome_configuration::Configuration { // editorconfig support is intentionally set to true, because prettier always reads the editorconfig file // see: https://github.com/prettier/prettier/issues/15255 use_editorconfig: Some(true.into()), + trailing_newline: None, }; result.formatter = Some(formatter); @@ -278,6 +279,7 @@ impl TryFrom for biome_configuration::Configuration { jsx_quote_style: Some(jsx_quote_style), attribute_position: Some(AttributePosition::default()), operator_linebreak: None, + trailing_newline: None, }; let js_config = biome_configuration::JsConfiguration { formatter: Some(js_formatter), diff --git a/crates/biome_cli/src/runner/impls/process_file/format.rs b/crates/biome_cli/src/runner/impls/process_file/format.rs index 06bf2d6878e9..a0e27bbfc0e5 100644 --- a/crates/biome_cli/src/runner/impls/process_file/format.rs +++ b/crates/biome_cli/src/runner/impls/process_file/format.rs @@ -132,6 +132,7 @@ impl ProcessFile for FormatProcessFile { execution: _, skip_ignore_check, } = payload; + let FileFeaturesResult { features_supported: file_features, } = workspace.file_features(SupportsFeatureParams { diff --git a/crates/biome_cli/tests/cases/editorconfig.rs b/crates/biome_cli/tests/cases/editorconfig.rs index 10a6d2c3c534..bf93dfa116eb 100644 --- a/crates/biome_cli/tests/cases/editorconfig.rs +++ b/crates/biome_cli/tests/cases/editorconfig.rs @@ -698,3 +698,50 @@ fn indent_size_can_set_to_tab() { result, )); } + +#[test] +fn should_support_insert_final_newline() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let editorconfig = Utf8Path::new(".editorconfig"); + fs.insert( + editorconfig.into(), + r#" +[*] +insert_final_newline = false +"#, + ); + + let biomeconfig = Utf8Path::new("biome.json"); + fs.insert( + biomeconfig.into(), + r#"{ + "formatter": { + "useEditorconfig": true + } +} +"#, + ); + + let test_file = Utf8Path::new("test.js"); + let contents = r#"function test() { + console.log("no newline")}"#; + fs.insert(test_file.into(), contents); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["format", test_file.as_str()].as_slice()), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "should_support_insert_final_newline", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/commands/format.rs b/crates/biome_cli/tests/commands/format.rs index 5e0be5964f0d..2842a1b75066 100644 --- a/crates/biome_cli/tests/commands/format.rs +++ b/crates/biome_cli/tests/commands/format.rs @@ -1228,7 +1228,7 @@ fn format_stdin_formats_virtual_path_outside_includes() { let (fs, result) = run_cli( fs, &mut console, - Args::from(["format", "--stdin-file-path", "a.tsx"].as_slice()), + Args::from(["format", "--stdin-file-path=a.tsx"].as_slice()), ); assert!(result.is_ok(), "run_cli returned {result:?}"); @@ -3616,3 +3616,421 @@ fn should_format_file_with_syntax_errors_when_flag_enabled() { result, )); } + +#[test] +fn trailing_newline_javascript_via_config() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + let file_path = Utf8Path::new("biome.json"); + fs.insert( + file_path.into(), + r#"{ + "files": { + "includes": ["**/*.js"] + }, + "javascript": { + "formatter": { + "trailingNewline": false + } + } +} +"# + .as_bytes(), + ); + + let test = Utf8Path::new("test.js"); + fs.insert(test.into(), "const a = 1;\n".as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["format", "--write", test.as_str()].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_file_contents(&fs, test, "const a = 1;"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_javascript_via_config", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_javascript_via_cli() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + + let test = Utf8Path::new("test.js"); + fs.insert(test.into(), "const a = 1;\n".as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "format", + "--write", + "--javascript-formatter-trailing-newline=false", + test.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_file_contents(&fs, test, "const a = 1;"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_javascript_via_cli", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_json_via_config() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + let file_path = Utf8Path::new("biome.json"); + fs.insert( + file_path.into(), + r#"{ + "json": { + "formatter": { + "trailingNewline": false + } + } +} +"# + .as_bytes(), + ); + + let test = Utf8Path::new("test.json"); + fs.insert(test.into(), r#"{"name": "test"}"#.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["format", "--write", test.as_str()].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + // Note: Just verify the snapshot, don't assert file contents + // The formatter will determine the actual formatting + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_json_via_config", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_json_via_cli() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + + let test = Utf8Path::new("test.json"); + fs.insert(test.into(), r#"{"name": "test"}"#.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "format", + "--write", + "--json-formatter-trailing-newline=false", + test.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + // Note: Just verify the snapshot, don't assert file contents + // The formatter will determine the actual formatting + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_json_via_cli", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_css_via_config() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + let file_path = Utf8Path::new("biome.json"); + fs.insert( + file_path.into(), + r#"{ + "files": { + "includes": ["**/*.css"] + }, + "css": { + "formatter": { + "trailingNewline": false + } + } +} +"# + .as_bytes(), + ); + + let test = Utf8Path::new("test.css"); + fs.insert(test.into(), ".test { color: red; }".as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["format", "--write", test.as_str()].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_file_contents(&fs, test, ".test {\n\tcolor: red;\n}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_css_via_config", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_css_via_cli() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + + let test = Utf8Path::new("test.css"); + fs.insert(test.into(), ".test { color: red; }".as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "format", + "--write", + "--css-formatter-trailing-newline=false", + test.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_file_contents(&fs, test, ".test {\n\tcolor: red;\n}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_css_via_cli", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_graphql_via_config() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + let file_path = Utf8Path::new("biome.json"); + fs.insert( + file_path.into(), + r#"{ + "files": { + "includes": ["**/*.graphql"] + }, + "graphql": { + "formatter": { + "trailingNewline": false + } + } +} +"# + .as_bytes(), + ); + + let test = Utf8Path::new("test.graphql"); + fs.insert(test.into(), "type Query { hello: String }".as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["format", "--write", test.as_str()].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_file_contents(&fs, test, "type Query {\n\thello: String\n}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_graphql_via_config", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_graphql_via_cli() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + + let test = Utf8Path::new("test.graphql"); + fs.insert(test.into(), "type Query { hello: String }".as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "format", + "--write", + "--graphql-formatter-trailing-newline=false", + test.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_file_contents(&fs, test, "type Query {\n\thello: String\n}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_graphql_via_cli", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_html_via_config() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + let file_path = Utf8Path::new("biome.json"); + fs.insert( + file_path.into(), + r#"{ + "files": { + "includes": ["**/*.html"] + }, + "html": { + "formatter": { + "enabled": true, + "trailingNewline": false + } + } +} +"# + .as_bytes(), + ); + + let test = Utf8Path::new("test.html"); + fs.insert( + test.into(), + "Hello".as_bytes(), + ); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["format", "--write", test.as_str()].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_file_contents( + &fs, + test, + "\n\n\tHello\n", + ); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_html_via_config", + fs, + console, + result, + )); +} + +#[test] +fn trailing_newline_html_via_cli() { + let mut console = BufferConsole::default(); + let fs = MemoryFileSystem::default(); + + let file_path = Utf8Path::new("biome.json"); + fs.insert( + file_path.into(), + r#"{ + "html": { + "formatter": { + "enabled": true + } + } +} +"# + .as_bytes(), + ); + + let test = Utf8Path::new("test.html"); + fs.insert( + test.into(), + "Hello".as_bytes(), + ); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "format", + "--write", + "--html-formatter-trailing-newline=false", + test.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + assert_file_contents( + &fs, + test, + "\n\n\tHello\n", + ); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "trailing_newline_html_via_cli", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/snapshots/main_cases_editorconfig/should_support_insert_final_newline.snap b/crates/biome_cli/tests/snapshots/main_cases_editorconfig/should_support_insert_final_newline.snap new file mode 100644 index 000000000000..8ed58d929900 --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_cases_editorconfig/should_support_insert_final_newline.snap @@ -0,0 +1,60 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: redactor(content) +--- +## `biome.json` + +```json +{ + "formatter": { + "useEditorconfig": true + } +} +``` + +## `.editorconfig` + +```editorconfig + +[*] +insert_final_newline = false + +``` + +## `test.js` + +```js +function test() { + console.log("no newline")} +``` + +# Termination Message + +```block +format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. + + + +``` + +# Emitted Messages + +```block +test.js format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Formatter would have printed the following content: + + 1 1 │ function test() { + 2 │ - ····console.log("no·newline")} + 2 │ + → console.log("no·newline"); + 3 │ + } + + +``` + +```block +Checked 1 file in