diff --git a/.changeset/fix-stdin-file-path-includes.md b/.changeset/fix-stdin-file-path-includes.md new file mode 100644 index 000000000000..7a553e8f064e --- /dev/null +++ b/.changeset/fix-stdin-file-path-includes.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fix `--stdin-file-path` being blocked by `includes` configuration. Stdin input now bypasses all path-based ignore checks (`files.includes`, `formatter.includes`, VCS ignore). The path is only used for language detection, since stdin content is explicitly provided by the user. diff --git a/crates/biome_cli/src/execute/process_file.rs b/crates/biome_cli/src/execute/process_file.rs index 978d3c64657f..5a22108bbebc 100644 --- a/crates/biome_cli/src/execute/process_file.rs +++ b/crates/biome_cli/src/execute/process_file.rs @@ -136,6 +136,7 @@ pub(crate) fn process_file(ctx: &TraversalOptions, biome_path: &BiomePath) -> Fi project_key: ctx.project_key, path: biome_path.clone(), features: ctx.execution.to_feature(), + is_stdin: false, }) .with_file_path_and_code_and_tags( biome_path.to_string(), diff --git a/crates/biome_cli/src/execute/std_in.rs b/crates/biome_cli/src/execute/std_in.rs index 6972f7750eee..baf99408ff71 100644 --- a/crates/biome_cli/src/execute/std_in.rs +++ b/crates/biome_cli/src/execute/std_in.rs @@ -45,6 +45,7 @@ pub(crate) fn run<'a>( project_key, path: biome_path.clone(), features: FeaturesBuilder::new().with_formatter().build(), + is_stdin: true, })?; if file_features.is_ignored() { @@ -126,6 +127,7 @@ pub(crate) fn run<'a>( .with_assist() .with_formatter() .build(), + is_stdin: true, })?; if file_features.is_ignored() { diff --git a/crates/biome_cli/src/execute/traverse.rs b/crates/biome_cli/src/execute/traverse.rs index 26968c1821c8..929bbe0729c4 100644 --- a/crates/biome_cli/src/execute/traverse.rs +++ b/crates/biome_cli/src/execute/traverse.rs @@ -576,6 +576,7 @@ impl TraversalContext for TraversalOptions<'_, '_> { project_key: self.project_key, path: biome_path.clone(), features: self.execution.to_feature(), + is_stdin: false, }); let can_read = DocumentFileSource::can_read(biome_path); diff --git a/crates/biome_cli/tests/commands/format.rs b/crates/biome_cli/tests/commands/format.rs index 2ea9e8ba8291..66b5a125ba41 100644 --- a/crates/biome_cli/tests/commands/format.rs +++ b/crates/biome_cli/tests/commands/format.rs @@ -1293,6 +1293,52 @@ fn format_stdin_does_not_error_with_ignore_unknown_file_extensions() { )); } +#[test] +fn format_stdin_ignores_includes_pattern() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + // Set up a config with includes that doesn't match our stdin path + fs.insert( + Utf8Path::new("biome.json").into(), + r#"{ "formatter": { "includes": ["apps/**"] } }"#.as_bytes(), + ); + + // stdin path is "mock.js" which doesn't match "apps/**" + // But stdin should bypass includes check + console + .in_buffer + .push("function f() {return{}}".to_string()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["format", "--stdin-file-path", "mock.js"].as_slice()), + ); + + assert!(result.is_ok(), "run_cli returned {result:?}"); + + let message = console + .out_buffer + .first() + .expect("Console should have written a message"); + + let content = markup_to_string(markup! { + {message.content} + }); + + // Verify the content was formatted despite not matching includes pattern + assert_eq!(content, "function f() {\n\treturn {};\n}\n"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "format_stdin_ignores_includes_pattern", + fs, + console, + result, + )); +} + #[test] fn does_not_format_if_disabled() { let fs = MemoryFileSystem::default(); diff --git a/crates/biome_cli/tests/snapshots/main_commands_format/format_stdin_ignores_includes_pattern.snap b/crates/biome_cli/tests/snapshots/main_commands_format/format_stdin_ignores_includes_pattern.snap new file mode 100644 index 000000000000..b0b035af6cd0 --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_commands_format/format_stdin_ignores_includes_pattern.snap @@ -0,0 +1,24 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: redactor(content) +--- +## `biome.json` + +```json +{ "formatter": { "includes": ["apps/**"] } } +``` + +# Input messages + +```block +function f() {return{}} +``` + +# Emitted Messages + +```block +function f() { + return {}; +} + +``` diff --git a/crates/biome_formatter_test/src/spec.rs b/crates/biome_formatter_test/src/spec.rs index 18035fa9f55c..3c930b08fbf1 100644 --- a/crates/biome_formatter_test/src/spec.rs +++ b/crates/biome_formatter_test/src/spec.rs @@ -70,6 +70,7 @@ impl<'a> SpecTestFile<'a> { project_key, path: input_file.clone(), features: FeaturesBuilder::new().with_formatter().build(), + is_stdin: false, }) .unwrap(); diff --git a/crates/biome_lsp/src/handlers/analysis.rs b/crates/biome_lsp/src/handlers/analysis.rs index 1292c7be6b13..c6fa3872f401 100644 --- a/crates/biome_lsp/src/handlers/analysis.rs +++ b/crates/biome_lsp/src/handlers/analysis.rs @@ -78,6 +78,7 @@ pub(crate) fn code_actions( project_key: doc.project_key, path: path.clone(), features, + is_stdin: false, })?; if !file_features.supports_lint() && !file_features.supports_assist() { @@ -315,6 +316,7 @@ fn fix_all( .with_linter() .with_assist() .build(), + is_stdin: false, })?; let should_format = file_features.supports_format(); diff --git a/crates/biome_lsp/src/handlers/formatting.rs b/crates/biome_lsp/src/handlers/formatting.rs index 42eb49f42105..87aff02eaaf1 100644 --- a/crates/biome_lsp/src/handlers/formatting.rs +++ b/crates/biome_lsp/src/handlers/formatting.rs @@ -38,6 +38,7 @@ pub(crate) fn format( project_key: doc.project_key, path: path.clone(), features, + is_stdin: false, })?; if !file_features.supports_format() { return notify_user(file_features, path); @@ -107,6 +108,7 @@ pub(crate) fn format_range( project_key: doc.project_key, path: path.clone(), features, + is_stdin: false, })?; if !file_features.supports_format() { return notify_user(file_features, path); @@ -199,6 +201,7 @@ pub(crate) fn format_on_type( project_key: doc.project_key, path: path.clone(), features, + is_stdin: false, })?; if !file_features.supports_format() { return notify_user(file_features, path); diff --git a/crates/biome_lsp/src/session.rs b/crates/biome_lsp/src/session.rs index 7381c11cdf77..38a3e0aef154 100644 --- a/crates/biome_lsp/src/session.rs +++ b/crates/biome_lsp/src/session.rs @@ -422,6 +422,7 @@ impl Session { project_key: doc.project_key, features: FeaturesBuilder::new().with_linter().with_assist().build(), path: biome_path.clone(), + is_stdin: false, })?; if !file_features.supports_lint() && !file_features.supports_assist() { diff --git a/crates/biome_service/src/projects.rs b/crates/biome_service/src/projects.rs index 050583ce5dcd..3c3f17793687 100644 --- a/crates/biome_service/src/projects.rs +++ b/crates/biome_service/src/projects.rs @@ -198,6 +198,7 @@ impl Projects { is_ignored_by_top_level_config || is_ignored_by_features } + #[expect(clippy::too_many_arguments)] #[inline(always)] pub fn get_file_features( &self, @@ -207,6 +208,7 @@ impl Projects { features: FeatureName, language: DocumentFileSource, capabilities: &Capabilities, + is_stdin: bool, ) -> Result { let data = self.0.pin(); let project_data = data @@ -231,9 +233,15 @@ impl Projects { .is_some_and(|dir_path| dir_path == project_data.path) { // Never ignore Biome's top-level config file - } else if self.is_ignored(fs, project_key, path, features, IgnoreKind::Ancestors) { + } else if !is_stdin + && self.is_ignored(fs, project_key, path, features, IgnoreKind::Ancestors) + { + // Skip ignore checks for stdin - the path is only used for language detection, + // not for path matching. Stdin content is explicitly provided, so VCS ignore + // and includes patterns should not apply. file_features.set_ignored_for_all_features(); - } else { + } else if !is_stdin { + // Skip feature-specific includes check for stdin. for feature in features.iter() { if project_data .root_settings diff --git a/crates/biome_service/src/workspace.rs b/crates/biome_service/src/workspace.rs index 942ff9957247..781af26fafee 100644 --- a/crates/biome_service/src/workspace.rs +++ b/crates/biome_service/src/workspace.rs @@ -116,6 +116,11 @@ pub struct SupportsFeatureParams { pub project_key: ProjectKey, pub path: BiomePath, pub features: FeatureName, + /// When true, skip ignore and `includes` pattern checks. Used for stdin + /// input where the path is only used for language detection. Stdin content + /// is explicitly provided, so VCS ignore and includes patterns should not apply. + #[serde(default)] + pub is_stdin: bool, } #[derive(Debug, serde::Serialize, serde::Deserialize, Default)] diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 3ff152f17737..92cb7a653a40 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -1170,6 +1170,7 @@ impl Workspace for WorkspaceServer { params.features, language, &capabilities, + params.is_stdin, ) } diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 9b4a26233c87..630892f9c563 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -2,6 +2,12 @@ import type { Transport } from "./transport"; export interface SupportsFeatureParams { features: FeatureName; + /** + * When true, skip ignore and `includes` pattern checks. Used for stdin +input where the path is only used for language detection. Stdin content +is explicitly provided, so VCS ignore and includes patterns should not apply. + */ + isStdin?: boolean; path: BiomePath; projectKey: ProjectKey; }