diff --git a/crates/ty_project/src/glob/exclude.rs b/crates/ty_project/src/glob/exclude.rs index c7f6e625b9..7bea3ffc4a 100644 --- a/crates/ty_project/src/glob/exclude.rs +++ b/crates/ty_project/src/glob/exclude.rs @@ -20,7 +20,7 @@ use crate::glob::portable::AbsolutePortableGlobPattern; /// /// # Equality /// -/// Two filters are equal if they're constructed from the same patterns (including order). +/// Two filters are equal if they're constructed from the same absolute globs (including order). /// Two filters that exclude the exact same files but were constructed from different patterns aren't considered /// equal. #[derive(Clone, Debug, PartialEq, Eq, get_size2::GetSize)] @@ -112,7 +112,7 @@ impl ExcludeFilterBuilder { /// /// # Equality /// -/// Two ignore matches are only equal if they're constructed from the same patterns (including order). +/// Two ignore matchers are only equal if they're constructed from the same absolute globs (including order). /// Two matchers that were constructed from different patterns but result in /// including the same files don't compare equal. #[derive(Clone, get_size2::GetSize)] @@ -192,7 +192,7 @@ impl Match { #[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)] struct IgnoreGlob { - /// The pattern that was originally parsed. + /// The original absolute glob, before removing negation or a trailing slash. original: String, /// This is a pattern allowing a path (it starts with a `!`, possibly undoing a previous ignore) @@ -247,7 +247,7 @@ impl GitignoreBuilder { pattern: &AbsolutePortableGlobPattern, ) -> Result<&mut GitignoreBuilder, globset::Error> { let mut glob = IgnoreGlob { - original: pattern.relative().to_string(), + original: pattern.absolute().to_string(), is_allow: false, is_only_dir: false, }; @@ -291,3 +291,32 @@ impl GitignoreBuilder { Ok(self) } } + +#[cfg(test)] +mod tests { + use super::{ExcludeFilter, ExcludeFilterBuilder}; + use crate::GlobFilterCheckMode; + use crate::glob::{PortableGlobKind, PortableGlobPattern}; + use ruff_db::system::SystemPath; + + #[test] + fn equality_accounts_for_pattern_root() -> anyhow::Result<()> { + let build = |root| -> anyhow::Result { + let mut builder = ExcludeFilterBuilder::new(); + builder.add( + &PortableGlobPattern::parse("**/.venv/", PortableGlobKind::Exclude)? + .into_absolute(root), + )?; + Ok(builder.build()?) + }; + let member = build("/workspace/project")?; + let workspace = build("/workspace")?; + let environment = SystemPath::new("/workspace/.venv"); + + assert!(!member.match_directory(environment, GlobFilterCheckMode::Adhoc)); + assert!(workspace.match_directory(environment, GlobFilterCheckMode::Adhoc)); + assert_ne!(member, workspace); + assert_eq!(workspace, build("/workspace")?); + Ok(()) + } +} diff --git a/crates/ty_project/src/glob/include.rs b/crates/ty_project/src/glob/include.rs index 6ac4c61b5f..b2147aa36e 100644 --- a/crates/ty_project/src/glob/include.rs +++ b/crates/ty_project/src/glob/include.rs @@ -29,7 +29,7 @@ const DFA_SIZE_LIMIT: usize = 1_000_000; /// regex allows to check for prefix matches. /// /// ## Equality -/// Equality is based on the patterns from which a filter was constructed. +/// Equality is based on the original absolute globs. /// /// Because of that, two filters that include the exact same files but were /// constructed from different patterns (or even just order) compare unequal. @@ -37,6 +37,7 @@ const DFA_SIZE_LIMIT: usize = 1_000_000; pub(crate) struct IncludeFilter { #[get_size(ignore)] glob_set: GlobSet, + /// The original absolute globs, before adding patterns for descendants. original_patterns: Box<[Box]>, #[get_size(size_fn = bit_box_size)] literal_pattern_indices: BitBox, @@ -216,7 +217,7 @@ impl IncludeFilterBuilder { .backslash_escape(true) .build()?; - self.original_patterns.push(input.relative().into()); + self.original_patterns.push(input.absolute().into()); // `lib` is the same as `lib/**` // Add a glob that matches `lib` exactly, change the glob to `lib/**`. @@ -355,6 +356,28 @@ mod tests { assert!(!filter.match_directory(path.replace('/', MAIN_SEPARATOR_STR))); } + #[test] + fn equality_accounts_for_pattern_root() -> anyhow::Result<()> { + let build = |root| -> anyhow::Result { + let mut builder = IncludeFilterBuilder::new(); + builder.add( + &PortableGlobPattern::parse("src", PortableGlobKind::Include)?.into_absolute(root), + )?; + Ok(builder.build()?) + }; + let member = build("/workspace/project")?; + let workspace = build("/workspace")?; + + assert_eq!(member.match_file("/workspace/src/main.py"), MatchFile::No); + assert_eq!( + workspace.match_file("/workspace/src/main.py"), + MatchFile::Pattern + ); + assert_ne!(member, workspace); + assert_eq!(workspace, build("/workspace")?); + Ok(()) + } + #[test] fn match_directory() { // `lib` is the same as `src/**`. It includes a file or directory (including its contents) diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index 1210fcfbb4..c8702a7f13 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -1694,6 +1694,11 @@ impl TestContext { .map_err(|()| anyhow!("Failed to convert root directory to uri"))?; settings.add_filter(&tempdir_filter(project_dir.as_str()), "/"); settings.add_filter(&tempdir_filter(project_dir_uri.path()), "/"); + // Absolute globs use forward slashes on every platform. + settings.add_filter( + &tempdir_filter(project_dir.as_str().replace('\\', "/")), + "/", + ); settings.add_filter(r#"\\\\"#, "/"); settings.add_filter( r#"The system cannot find the file specified."#, diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap index 636cab6743..79a274333b 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -58,102 +58,102 @@ Settings: Settings { ignore: Gitignore( [ IgnoreGlob { - original: "**/.bzr/", + original: "/**/.bzr/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.direnv/", + original: "/**/.direnv/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.eggs/", + original: "/**/.eggs/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.git/", + original: "/**/.git/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.git-rewrite/", + original: "/**/.git-rewrite/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.hg/", + original: "/**/.hg/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.mypy_cache/", + original: "/**/.mypy_cache/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.nox/", + original: "/**/.nox/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.pants.d/", + original: "/**/.pants.d/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.pytype/", + original: "/**/.pytype/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.ruff_cache/", + original: "/**/.ruff_cache/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.svn/", + original: "/**/.svn/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.tox/", + original: "/**/.tox/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/.venv/", + original: "/**/.venv/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/__pypackages__/", + original: "/**/__pypackages__/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/_build/", + original: "/**/_build/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/buck-out/", + original: "/**/buck-out/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/dist/", + original: "/**/dist/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/node_modules/", + original: "/**/node_modules/", is_allow: false, is_only_dir: true, }, IgnoreGlob { - original: "**/venv/", + original: "/**/venv/", is_allow: false, is_only_dir: true, },