mirror of https://github.com/astral-sh/ruff
[ty] Add `--force-exclude` option (#22076)
This commit is contained in:
parent
2a959ef3f2
commit
5b475b45aa
|
|
@ -56,6 +56,7 @@ over all configuration files.</p>
|
|||
</dd><dt id="ty-check--exit-zero"><a href="#ty-check--exit-zero"><code>--exit-zero</code></a></dt><dd><p>Always use exit code 0, even when there are error-level diagnostics</p>
|
||||
</dd><dt id="ty-check--extra-search-path"><a href="#ty-check--extra-search-path"><code>--extra-search-path</code></a> <i>path</i></dt><dd><p>Additional path to use as a module-resolution source (can be passed multiple times).</p>
|
||||
<p>This is an advanced option that should usually only be used for first-party or third-party modules that are not installed into your Python environment in a conventional way. Use <code>--python</code> to point ty to your Python environment if it is in an unusual location.</p>
|
||||
</dd><dt id="ty-check--force-exclude"><a href="#ty-check--force-exclude"><code>--force-exclude</code></a></dt><dd><p>Enforce exclusions, even for paths passed to ty directly on the command-line. Use <code>--no-force-exclude</code> to disable</p>
|
||||
</dd><dt id="ty-check--help"><a href="#ty-check--help"><code>--help</code></a>, <code>-h</code></dt><dd><p>Print help (see a summary with '-h')</p>
|
||||
</dd><dt id="ty-check--ignore"><a href="#ty-check--ignore"><code>--ignore</code></a> <i>rule</i></dt><dd><p>Disables the rule. Can be specified multiple times.</p>
|
||||
</dd><dt id="ty-check--no-progress"><a href="#ty-check--no-progress"><code>--no-progress</code></a></dt><dd><p>Hide all progress outputs.</p>
|
||||
|
|
|
|||
|
|
@ -154,6 +154,17 @@ pub(crate) struct CheckCommand {
|
|||
#[clap(long, overrides_with("respect_ignore_files"), hide = true)]
|
||||
no_respect_ignore_files: bool,
|
||||
|
||||
/// Enforce exclusions, even for paths passed to ty directly on the command-line.
|
||||
/// Use `--no-force-exclude` to disable.
|
||||
#[arg(
|
||||
long,
|
||||
overrides_with("no_force_exclude"),
|
||||
help_heading = "File selection"
|
||||
)]
|
||||
force_exclude: bool,
|
||||
#[clap(long, overrides_with("force_exclude"), hide = true)]
|
||||
no_force_exclude: bool,
|
||||
|
||||
/// Glob patterns for files to exclude from type checking.
|
||||
///
|
||||
/// Uses gitignore-style syntax to exclude files and directories from type checking.
|
||||
|
|
@ -178,6 +189,10 @@ pub(crate) struct CheckCommand {
|
|||
}
|
||||
|
||||
impl CheckCommand {
|
||||
pub(crate) fn force_exclude(&self) -> bool {
|
||||
resolve_bool_arg(self.force_exclude, self.no_progress).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn into_options(self) -> Options {
|
||||
let rules = if self.rules.is_empty() {
|
||||
None
|
||||
|
|
@ -435,3 +450,12 @@ impl ConfigsArg {
|
|||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
|
||||
match (yes, no) {
|
||||
(true, false) => Some(true),
|
||||
(false, true) => Some(false),
|
||||
(false, false) => None,
|
||||
(..) => unreachable!("Clap should make this impossible"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
|||
.config_file
|
||||
.as_ref()
|
||||
.map(|path| SystemPath::absolute(path, &cwd));
|
||||
let force_exclude = args.force_exclude();
|
||||
|
||||
let mut project_metadata = match &config_file {
|
||||
Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), &system)?,
|
||||
|
|
@ -130,11 +131,13 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
|||
project_metadata.apply_overrides(&project_options_overrides);
|
||||
|
||||
let mut db = ProjectDatabase::new(project_metadata, system)?;
|
||||
let project = db.project();
|
||||
|
||||
project.set_verbose(&mut db, verbosity >= VerbosityLevel::Verbose);
|
||||
project.set_force_exclude(&mut db, force_exclude);
|
||||
|
||||
db.project()
|
||||
.set_verbose(&mut db, verbosity >= VerbosityLevel::Verbose);
|
||||
if !check_paths.is_empty() {
|
||||
db.project().set_included_paths(&mut db, check_paths);
|
||||
project.set_included_paths(&mut db, check_paths);
|
||||
}
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) =
|
||||
|
|
|
|||
|
|
@ -589,6 +589,128 @@ fn explicit_path_overrides_exclude() -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Test behavior when explicitly checking a path that matches an exclude pattern and `--force-exclude` is provided
|
||||
#[test]
|
||||
fn explicit_path_overrides_exclude_force_exclude() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
(
|
||||
"src/main.py",
|
||||
r#"
|
||||
print(undefined_var) # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"tests/generated.py",
|
||||
r#"
|
||||
print(dist_undefined_var) # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"dist/other.py",
|
||||
r#"
|
||||
print(other_undefined_var) # error: unresolved-reference
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"ty.toml",
|
||||
r#"
|
||||
[src]
|
||||
exclude = ["tests/generated.py"]
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
// Explicitly checking a file in an excluded directory should still check that file
|
||||
assert_cmd_snapshot!(case.command().arg("tests/generated.py").arg("src/main.py"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> src/main.py:2:7
|
||||
|
|
||||
2 | print(undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `dist_undefined_var` used when not defined
|
||||
--> tests/generated.py:2:7
|
||||
|
|
||||
2 | print(dist_undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// Except when `--force-exclude` is set.
|
||||
assert_cmd_snapshot!(case.command().arg("tests/generated.py").arg("src/main.py").arg("--force-exclude"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> src/main.py:2:7
|
||||
|
|
||||
2 | print(undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// Explicitly checking the entire excluded directory should check all files in it
|
||||
assert_cmd_snapshot!(case.command().arg("dist/").arg("src/main.py"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `other_undefined_var` used when not defined
|
||||
--> dist/other.py:2:7
|
||||
|
|
||||
2 | print(other_undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> src/main.py:2:7
|
||||
|
|
||||
2 | print(undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 2 diagnostics
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// Except when using `--force-exclude`
|
||||
assert_cmd_snapshot!(case.command().arg("dist/").arg("src/main.py").arg("--force-exclude"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[unresolved-reference]: Name `undefined_var` used when not defined
|
||||
--> src/main.py:2:7
|
||||
|
|
||||
2 | print(undefined_var) # error: unresolved-reference
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_and_configuration_exclude() -> anyhow::Result<()> {
|
||||
let case = CliTest::with_files([
|
||||
|
|
|
|||
|
|
@ -115,6 +115,10 @@ pub struct Project {
|
|||
|
||||
#[default]
|
||||
verbose_flag: bool,
|
||||
|
||||
/// Whether to enforce exclusion rules even to files explicitly passed to ty on the command line.
|
||||
#[default]
|
||||
force_exclude_flag: bool,
|
||||
}
|
||||
|
||||
/// A progress reporter.
|
||||
|
|
@ -380,6 +384,16 @@ impl Project {
|
|||
self.verbose_flag(db)
|
||||
}
|
||||
|
||||
pub fn set_force_exclude(self, db: &mut dyn Db, force: bool) {
|
||||
if self.force_exclude_flag(db) != force {
|
||||
self.set_force_exclude_flag(db).to(force);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn force_exclude(self, db: &dyn Db) -> bool {
|
||||
self.force_exclude_flag(db)
|
||||
}
|
||||
|
||||
/// Returns the paths that should be checked.
|
||||
///
|
||||
/// The default is to check the entire project in which case this method returns
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ pub(crate) struct ProjectFilesWalker<'a> {
|
|||
walker: WalkDirectoryBuilder,
|
||||
|
||||
filter: ProjectFilesFilter<'a>,
|
||||
|
||||
force_exclude: bool,
|
||||
}
|
||||
|
||||
impl<'a> ProjectFilesWalker<'a> {
|
||||
|
|
@ -158,7 +160,11 @@ impl<'a> ProjectFilesWalker<'a> {
|
|||
walker = walker.add(path);
|
||||
}
|
||||
|
||||
Some(Self { walker, filter })
|
||||
Some(Self {
|
||||
walker,
|
||||
filter,
|
||||
force_exclude: db.project().force_exclude(db),
|
||||
})
|
||||
}
|
||||
|
||||
/// Walks the project paths and collects the paths of all files that
|
||||
|
|
@ -179,7 +185,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 {
|
||||
if entry.depth() > 0 || self.force_exclude {
|
||||
let directory_included = filter
|
||||
.is_directory_included(entry.path(), GlobFilterCheckMode::TopDown);
|
||||
return match directory_included {
|
||||
|
|
@ -218,7 +224,7 @@ impl<'a> ProjectFilesWalker<'a> {
|
|||
|
||||
// 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 {
|
||||
if entry.depth() > 0 || self.force_exclude {
|
||||
match filter
|
||||
.is_file_included(entry.path(), GlobFilterCheckMode::TopDown)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue