[ty] Fix anchoring of include and exclude patterns after the project's root changed (#28995)

This commit is contained in:
Micha Reiser authored and GitHub committed 2026-09-29 12:41:41 +00:00
1 parent 55c48b1e76
commit dab6370667
4 files changed
+83 -26

No files matched your search

+33 -4
View File
@@ -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<ExcludeFilter> {
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(())
}
}
+25 -2
View File
@@ -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<str>]>,
#[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<IncludeFilter> {
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)
+5
View File
@@ -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()), "<temp_dir>/");
settings.add_filter(&tempdir_filter(project_dir_uri.path()), "<temp_dir>/");
// Absolute globs use forward slashes on every platform.
settings.add_filter(
&tempdir_filter(project_dir.as_str().replace('\\', "/")),
"<temp_dir>/",
);
settings.add_filter(r#"\\\\"#, "/");
settings.add_filter(
r#"The system cannot find the file specified."#,
@@ -58,102 +58,102 @@ Settings: Settings {
ignore: Gitignore(
[
IgnoreGlob {
original: "**/.bzr/",
original: "<temp_dir>/**/.bzr/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.direnv/",
original: "<temp_dir>/**/.direnv/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.eggs/",
original: "<temp_dir>/**/.eggs/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.git/",
original: "<temp_dir>/**/.git/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.git-rewrite/",
original: "<temp_dir>/**/.git-rewrite/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.hg/",
original: "<temp_dir>/**/.hg/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.mypy_cache/",
original: "<temp_dir>/**/.mypy_cache/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.nox/",
original: "<temp_dir>/**/.nox/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.pants.d/",
original: "<temp_dir>/**/.pants.d/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.pytype/",
original: "<temp_dir>/**/.pytype/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.ruff_cache/",
original: "<temp_dir>/**/.ruff_cache/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.svn/",
original: "<temp_dir>/**/.svn/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.tox/",
original: "<temp_dir>/**/.tox/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/.venv/",
original: "<temp_dir>/**/.venv/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/__pypackages__/",
original: "<temp_dir>/**/__pypackages__/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/_build/",
original: "<temp_dir>/**/_build/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/buck-out/",
original: "<temp_dir>/**/buck-out/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/dist/",
original: "<temp_dir>/**/dist/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/node_modules/",
original: "<temp_dir>/**/node_modules/",
is_allow: false,
is_only_dir: true,
},
IgnoreGlob {
original: "**/venv/",
original: "<temp_dir>/**/venv/",
is_allow: false,
is_only_dir: true,
},