[ty] Fix --force-exclude when excluding entire directories (#22595)

This commit is contained in:
Micha Reiser
2026-01-16 09:23:26 +01:00
committed by GitHub
parent 0ce5ce4de1
commit 4adbf7798d
3 changed files with 94 additions and 16 deletions

View File

@@ -770,6 +770,77 @@ fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> {
Ok(())
}
/// Test that `--force-exclude` respects exclude patterns even for explicitly passed files.
#[test]
fn force_exclude_directory_exclusion() -> anyhow::Result<()> {
let case = CliTest::with_files([
(
"src/main.py",
r#"
print(undefined_var) # error: unresolved-reference
"#,
),
(
"out/amd64/install/_setup_util.py",
r#"
base_path: str = "/path"
if base_path not in CMAKE_PREFIX_PATH:
CMAKE_PREFIX_PATH.insert(0, base_path)
"#,
),
(
"ty.toml",
r#"
[src]
exclude = ["out"]
"#,
),
])?;
// Without --force-exclude, explicitly passed file overrides exclude.
assert_cmd_snapshot!(case.command().arg("out/amd64/install/_setup_util.py"), @r#"
success: false
exit_code: 1
----- stdout -----
error[unresolved-reference]: Name `CMAKE_PREFIX_PATH` used when not defined
--> out/amd64/install/_setup_util.py:3:21
|
2 | base_path: str = "/path"
3 | if base_path not in CMAKE_PREFIX_PATH:
| ^^^^^^^^^^^^^^^^^
4 | CMAKE_PREFIX_PATH.insert(0, base_path)
|
info: rule `unresolved-reference` is enabled by default
error[unresolved-reference]: Name `CMAKE_PREFIX_PATH` used when not defined
--> out/amd64/install/_setup_util.py:4:5
|
2 | base_path: str = "/path"
3 | if base_path not in CMAKE_PREFIX_PATH:
4 | CMAKE_PREFIX_PATH.insert(0, base_path)
| ^^^^^^^^^^^^^^^^^
|
info: rule `unresolved-reference` is enabled by default
Found 2 diagnostics
----- stderr -----
"#);
// With --force-exclude, the exclude pattern is enforced even for explicit paths.
assert_cmd_snapshot!(case.command().arg("--force-exclude").arg("out/amd64/install/_setup_util.py"), @"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
WARN No python files found under the given path(s)
");
Ok(())
}
#[test]
fn cli_and_configuration_exclude() -> anyhow::Result<()> {
let case = CliTest::with_files([

View File

@@ -75,7 +75,7 @@ impl std::fmt::Display for IncludeExcludeFilter {
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub(crate) enum GlobFilterCheckMode {
/// The paths are checked top-to-bottom and inclusion is determined
/// for each path during the traversal.

View File

@@ -21,6 +21,8 @@ pub(crate) struct ProjectFilesFilter<'a> {
/// The resolved `src.include` and `src.exclude` filter.
src_filter: &'a IncludeExcludeFilter,
force_exclude: bool,
}
impl<'a> ProjectFilesFilter<'a> {
@@ -28,9 +30,14 @@ impl<'a> ProjectFilesFilter<'a> {
Self {
included_paths: project.included_paths_or_root(db),
src_filter: &project.settings(db).src().files,
force_exclude: project.force_exclude(db),
}
}
pub(crate) fn force_exclude(&self) -> bool {
self.force_exclude
}
fn match_included_paths(
&self,
path: &SystemPath,
@@ -43,8 +50,8 @@ impl<'a> ProjectFilesFilter<'a> {
.iter()
.filter_map(|included_path| {
if let Ok(relative_path) = path.strip_prefix(included_path) {
// Exact matches are always included
if relative_path.as_str().is_empty() {
// Exact matches are always included, unless forced to exclude
if relative_path.as_str().is_empty() && !self.force_exclude {
Some(CheckPathMatch::Full)
} else {
Some(CheckPathMatch::Partial)
@@ -115,8 +122,6 @@ pub(crate) struct ProjectFilesWalker<'a> {
walker: WalkDirectoryBuilder,
filter: ProjectFilesFilter<'a>,
force_exclude: bool,
}
impl<'a> ProjectFilesWalker<'a> {
@@ -164,11 +169,7 @@ impl<'a> ProjectFilesWalker<'a> {
walker = walker.add(path);
}
Some(Self {
walker,
filter,
force_exclude: db.project().force_exclude(db),
})
Some(Self { walker, filter })
}
/// Walks the project paths and collects the paths of all files that
@@ -182,6 +183,7 @@ impl<'a> ProjectFilesWalker<'a> {
let filter = &self.filter;
let files = &files;
let diagnostics = &diagnostics;
let force_exclude = filter.force_exclude();
Box::new(move |entry| {
match entry {
@@ -189,7 +191,7 @@ impl<'a> ProjectFilesWalker<'a> {
// Skip excluded directories unless they were explicitly passed to the walker
// (which is the case passed to `ty check <paths>`).
if entry.file_type().is_directory() {
if entry.depth() > 0 || self.force_exclude {
if entry.depth() > 0 || force_exclude {
let directory_included = filter
.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown);
return match directory_included {
@@ -213,9 +215,14 @@ impl<'a> ProjectFilesWalker<'a> {
} else {
// For all files, except the ones that were explicitly passed to the walker (CLI),
// check if they're included in the project.
if entry.depth() > 0 || self.force_exclude {
if entry.depth() > 0 || force_exclude {
let match_mode = if entry.depth() == 0 && force_exclude {
GlobFilterCheckMode::Adhoc
} else {
GlobFilterCheckMode::TopDown
};
match filter
.is_file_included(entry.path(), GlobFilterCheckMode::TopDown)
.is_file_included(entry.path(), match_mode)
{
IncludeResult::Included { literal_match } => {
// Ignore any non python files to avoid creating too many entries in `Files`.
@@ -229,7 +236,7 @@ impl<'a> ProjectFilesWalker<'a> {
if source_type.is_none()
{
return WalkState::Continue;
return WalkState::Skip;
}
}
IncludeResult::Excluded => {
@@ -237,14 +244,14 @@ impl<'a> ProjectFilesWalker<'a> {
"Ignoring file `{path}` because it is excluded by a default or `src.exclude` pattern.",
path=entry.path()
);
return WalkState::Continue;
return WalkState::Skip;
}
IncludeResult::NotIncluded => {
tracing::debug!(
"Ignoring file `{path}` because it doesn't match any `src.include` pattern or path specified on the CLI.",
path=entry.path()
);
return WalkState::Continue;
return WalkState::Skip;
}
}
}