diff --git a/.changeset/ancient-ancestors-ascent.md b/.changeset/ancient-ancestors-ascent.md new file mode 100644 index 000000000000..b2ca8b061809 --- /dev/null +++ b/.changeset/ancient-ancestors-ascent.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#7268](https://github.com/biomejs/biome/issues/7268): Files that are explicitly passed as CLI arguments are now correctly ignored if they reside in an ignored folder. diff --git a/crates/biome_cli/src/execute/traverse.rs b/crates/biome_cli/src/execute/traverse.rs index 85405ea03e73..28f7512a4f14 100644 --- a/crates/biome_cli/src/execute/traverse.rs +++ b/crates/biome_cli/src/execute/traverse.rs @@ -541,7 +541,7 @@ impl TraversalContext for TraversalOptions<'_, '_> { project_key: self.project_key, path: biome_path.clone(), features: self.execution.to_feature(), - ignore_kind: IgnoreKind::Path, + ignore_kind: IgnoreKind::Ancestors, }) .unwrap_or_else(|err| { self.push_diagnostic(err.into()); diff --git a/crates/biome_cli/tests/cases/included_files.rs b/crates/biome_cli/tests/cases/included_files.rs index e4f74e72c0e1..6ed630487d35 100644 --- a/crates/biome_cli/tests/cases/included_files.rs +++ b/crates/biome_cli/tests/cases/included_files.rs @@ -96,11 +96,10 @@ fn does_not_handle_included_files_if_overridden_by_ignore() { fn does_not_handle_files_in_ignored_folder() { let mut console = BufferConsole::default(); let fs = MemoryFileSystem::default(); - let file_path = Utf8Path::new("biome.json"); fs.insert( - file_path.into(), + "biome.json".into(), r#"{ - "files": { "includes": ["test.js", "!**/folder"] } + "files": { "includes": ["**/*.js", "!**/folder"] } } "# .as_bytes(), diff --git a/crates/biome_cli/tests/commands/lint.rs b/crates/biome_cli/tests/commands/lint.rs index 9cc890d66110..a9a734160715 100644 --- a/crates/biome_cli/tests/commands/lint.rs +++ b/crates/biome_cli/tests/commands/lint.rs @@ -1215,7 +1215,7 @@ fn include_files_in_symlinked_subdir() { #[cfg(target_os = "windows")] { - check_windows_symlink!(symlink_file( + check_windows_symlink!(symlink_dir( root_path.join("symlinked"), subroot_path.join("symlink") )); @@ -1273,7 +1273,7 @@ fn ignore_file_in_subdir_in_symlinked_dir() { #[cfg(target_os = "windows")] { - check_windows_symlink!(symlink_file( + check_windows_symlink!(symlink_dir( root_path.join("symlinked"), subroot_path.join("symlink") )); diff --git a/crates/biome_cli/tests/snapshots/main_cases_included_files/does_not_handle_files_in_ignored_folder.snap b/crates/biome_cli/tests/snapshots/main_cases_included_files/does_not_handle_files_in_ignored_folder.snap index aa0ffc76c86d..260b3e865e26 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_included_files/does_not_handle_files_in_ignored_folder.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_included_files/does_not_handle_files_in_ignored_folder.snap @@ -6,7 +6,7 @@ expression: redactor(content) ```json { - "files": { "includes": ["test.js", "!**/folder"] } + "files": { "includes": ["**/*.js", "!**/folder"] } } ``` diff --git a/crates/biome_service/src/projects.rs b/crates/biome_service/src/projects.rs index d5c0cd65e41f..97e6675cc42f 100644 --- a/crates/biome_service/src/projects.rs +++ b/crates/biome_service/src/projects.rs @@ -4,7 +4,7 @@ use crate::settings::Settings; use crate::workspace::{ DocumentFileSource, FeatureName, FeaturesSupported, FileFeaturesResult, IgnoreKind, }; -use biome_fs::ConfigName; +use biome_fs::{ConfigName, FileSystem}; use camino::{Utf8Path, Utf8PathBuf}; use papaya::HashMap; use rustc_hash::FxBuildHasher; @@ -140,12 +140,15 @@ impl Projects { pub fn is_ignored_by_top_level_config( &self, + fs: &dyn FileSystem, project_key: ProjectKey, path: &Utf8Path, ignore_kind: IgnoreKind, ) -> bool { match self.0.pin().get(&project_key) { - Some(project_data) => is_ignored_by_top_level_config(project_data, path, ignore_kind), + Some(project_data) => { + is_ignored_by_top_level_config(fs, project_data, path, ignore_kind) + } None => false, } } @@ -153,6 +156,7 @@ impl Projects { #[inline] pub fn is_ignored( &self, + fs: &dyn FileSystem, project_key: ProjectKey, path: &Utf8Path, features: FeatureName, @@ -164,7 +168,7 @@ impl Projects { }; let is_ignored_by_top_level_config = - is_ignored_by_top_level_config(project_data, path, ignore_kind); + is_ignored_by_top_level_config(fs, project_data, path, ignore_kind); // If there are specific features enabled, but all of them ignore the // path, then we treat the path as ignored too. @@ -181,6 +185,7 @@ impl Projects { #[inline(always)] pub fn get_file_features( &self, + fs: &dyn FileSystem, project_key: ProjectKey, path: &Utf8Path, features: FeatureName, @@ -211,12 +216,7 @@ impl Projects { .is_some_and(|dir_path| dir_path == project_data.path) { // Never ignore Biome's top-level config file - } else if !settings.files.includes.is_included(path) - || project_data - .root_settings - .vcs_settings - .is_ignored(path, Some(project_data.path.as_path())) - { + } else if self.is_ignored(fs, project_key, path, features, IgnoreKind::Ancestors) { file_features.set_ignored_for_all_features(); } else { for feature in features.iter() { @@ -340,6 +340,7 @@ impl Projects { #[inline] fn is_ignored_by_top_level_config( + fs: &dyn FileSystem, project_data: &ProjectData, path: &Utf8Path, ignore_kind: IgnoreKind, @@ -354,7 +355,22 @@ fn is_ignored_by_top_level_config( &project_data.root_settings.files.includes, |(_, settings)| &settings.files.includes, ); - let is_included = includes.is_included(path); + let mut is_included = if fs.path_is_dir(path) { + includes.is_dir_included(path) + } else { + includes.is_file_included(path) + }; + + // If necessary, check all the ancestors too. + if ignore_kind == IgnoreKind::Ancestors { + for ancestor in path.ancestors().skip(1) { + if !is_included || ancestor == project_data.path { + break; + } + + is_included = is_included && includes.is_dir_included(ancestor) + } + } let root_path = match ignore_kind { IgnoreKind::Ancestors => Some(project_data.path.as_path()), diff --git a/crates/biome_service/src/settings.rs b/crates/biome_service/src/settings.rs index 20131ce3ad7f..58a9c39e8da8 100644 --- a/crates/biome_service/src/settings.rs +++ b/crates/biome_service/src/settings.rs @@ -311,6 +311,8 @@ impl Settings { /// Returns whether the given `path` is ignored for the given `feature`, /// based on the current settings. + /// + /// `path` is expected to point to a file and not a directory. #[inline] pub fn is_path_ignored_for_feature(&self, path: &Utf8Path, feature: FeatureKind) -> bool { let feature_includes_files = match feature { @@ -321,7 +323,11 @@ impl Settings { FeatureKind::Debug => return false, }; - !feature_includes_files.is_included(path) + if is_dir(path) { + !feature_includes_files.is_dir_included(path) + } else { + !feature_includes_files.is_file_included(path) + } } } @@ -933,15 +939,22 @@ impl Includes { current_globs.extend(globs.into()); } - /// Returns whether the given `path` is included. + /// Returns whether the given `file_path` is included. + /// + /// `file_path` must point to an ordinary file. If it is a directory, you + /// should use [Self::is_dir_included()] instead. #[inline] - pub fn is_included(&self, path: &Utf8Path) -> bool { - self.is_unset() - || if is_dir(path) { - self.matches_directory_with_exceptions(path) - } else { - self.matches_with_exceptions(path) - } + pub fn is_file_included(&self, file_path: &Utf8Path) -> bool { + self.is_unset() || self.matches_with_exceptions(file_path) + } + + /// Returns whether the given `dir_path` is included. + /// + /// `file_path` must point to a directory. If it is a file, you should use + /// [Self::is_file_included()] instead. + #[inline] + pub fn is_dir_included(&self, dir_path: &Utf8Path) -> bool { + self.is_unset() || self.matches_directory_with_exceptions(dir_path) } /// Returns `true` is no globs are set. diff --git a/crates/biome_service/src/workspace.rs b/crates/biome_service/src/workspace.rs index 1dc6a8d1f1d2..332f5276be3f 100644 --- a/crates/biome_service/src/workspace.rs +++ b/crates/biome_service/src/workspace.rs @@ -1212,7 +1212,7 @@ pub struct PathIsIgnoredParams { /// Controls how to ignore check should be done pub ignore_kind: IgnoreKind, } -#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] pub enum IgnoreKind { diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 788869f3d1b4..c7e9f68010bc 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -574,10 +574,12 @@ impl WorkspaceServer { return Ok(!scan_kind.is_project()); } - if self - .projects - .is_ignored_by_top_level_config(project_key, &path, ignore_kind) - { + if self.projects.is_ignored_by_top_level_config( + self.fs.as_ref(), + project_key, + &path, + ignore_kind, + ) { return Ok(true); // Nobody cares about ignored paths. } @@ -606,6 +608,7 @@ impl WorkspaceServer { IgnoreKind::Path => !path.is_required_during_scan(), IgnoreKind::Ancestors => path.parent().is_none_or(|folder_path| { self.projects.is_ignored_by_top_level_config( + self.fs.as_ref(), project_key, folder_path, ignore_kind, @@ -629,6 +632,7 @@ impl WorkspaceServer { IgnoreKind::Path => false, IgnoreKind::Ancestors => path.parent().is_none_or(|folder_path| { self.projects.is_ignored_by_top_level_config( + self.fs.as_ref(), project_key, folder_path, ignore_kind, @@ -637,6 +641,7 @@ impl WorkspaceServer { } } else { self.projects.is_ignored_by_top_level_config( + self.fs.as_ref(), project_key, &path, ignore_kind, @@ -967,6 +972,7 @@ impl Workspace for WorkspaceServer { let capabilities = self.features.get_capabilities(language); self.projects.get_file_features( + self.fs.as_ref(), params.project_key, ¶ms.path, params.features, @@ -988,6 +994,7 @@ impl Workspace for WorkspaceServer { }; Ok(self.projects.is_ignored( + self.fs.as_ref(), params.project_key, ¶ms.path, params.features,