Enable opt-out of .gitignore checks via respect-gitignore flag (#1242)

This commit is contained in:
Charlie Marsh
2022-12-14 16:54:23 -05:00
committed by GitHub
parent 76891a8c07
commit 3f272b6cf8
11 changed files with 187 additions and 53 deletions

View File

@@ -86,10 +86,16 @@ pub struct Cli {
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// Respect file exclusions via `.gitignore` and other standard ignore
/// files.
#[arg(long, overrides_with("no_respect_gitignore"))]
respect_gitignore: bool,
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
no_respect_gitignore: bool,
/// See the files Ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
/// See the settings Ruff used for the first matching file.
/// See the settings Ruff will use to check a given Python file.
#[arg(long)]
pub show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
@@ -156,6 +162,10 @@ impl Cli {
line_length: self.line_length,
max_complexity: self.max_complexity,
per_file_ignores: self.per_file_ignores,
respect_gitignore: resolve_bool_arg(
self.respect_gitignore,
self.no_respect_gitignore,
),
select: self.select,
show_source: resolve_bool_arg(self.show_source, self.no_show_source),
target_version: self.target_version,
@@ -212,6 +222,7 @@ pub struct Overrides {
pub line_length: Option<usize>,
pub max_complexity: Option<usize>,
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub respect_gitignore: Option<bool>,
pub select: Option<Vec<CheckCodePrefix>>,
pub show_source: Option<bool>,
pub target_version: Option<PythonVersion>,

View File

@@ -18,20 +18,22 @@ use crate::iterators::par_iter;
use crate::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
use crate::message::Message;
use crate::resolver;
use crate::resolver::Strategy;
use crate::resolver::{FileDiscovery, PyprojectDiscovery};
use crate::settings::types::SerializationFormat;
/// Run the linter over a collection of files.
pub fn run(
files: &[PathBuf],
strategy: &Strategy,
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
cache: bool,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
// Collect all the files to check.
let start = Instant::now();
let (paths, resolver) = resolver::python_files_in_path(files, strategy, overrides)?;
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
@@ -41,7 +43,7 @@ pub fn run(
match entry {
Ok(entry) => {
let path = entry.path();
let settings = resolver.resolve(path, strategy);
let settings = resolver.resolve(path, pyproject_strategy);
lint_path(path, settings, &cache.into(), autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
@@ -57,7 +59,7 @@ pub fn run(
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, strategy);
let settings = resolver.resolve(path, pyproject_strategy);
if settings.enabled.contains(&CheckCode::E902) {
Diagnostics::new(vec![Message {
kind: CheckKind::IOError(message),
@@ -98,14 +100,14 @@ fn read_from_stdin() -> Result<String> {
/// Run the linter over a single file, read from `stdin`.
pub fn run_stdin(
strategy: &Strategy,
strategy: &PyprojectDiscovery,
filename: &Path,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
let stdin = read_from_stdin()?;
let settings = match strategy {
Strategy::Fixed(settings) => settings,
Strategy::Hierarchical(settings) => settings,
PyprojectDiscovery::Fixed(settings) => settings,
PyprojectDiscovery::Hierarchical(settings) => settings,
};
let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable();
@@ -113,10 +115,16 @@ pub fn run_stdin(
}
/// Add `noqa` directives to a collection of files.
pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<usize> {
pub fn add_noqa(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<usize> {
// Collect all the files to check.
let start = Instant::now();
let (paths, resolver) = resolver::python_files_in_path(files, strategy, overrides)?;
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
@@ -125,7 +133,7 @@ pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -
.flatten()
.filter_map(|entry| {
let path = entry.path();
let settings = resolver.resolve(path, strategy);
let settings = resolver.resolve(path, pyproject_strategy);
match add_noqa_to_path(path, settings) {
Ok(count) => Some(count),
Err(e) => {
@@ -143,10 +151,16 @@ pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -
}
/// Automatically format a collection of files.
pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<usize> {
pub fn autoformat(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<usize> {
// Collect all the files to format.
let start = Instant::now();
let (paths, resolver) = resolver::python_files_in_path(files, strategy, overrides)?;
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
@@ -155,7 +169,7 @@ pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides)
.flatten()
.filter_map(|entry| {
let path = entry.path();
let settings = resolver.resolve(path, strategy);
let settings = resolver.resolve(path, pyproject_strategy);
match autoformat_path(path, settings) {
Ok(()) => Some(()),
Err(e) => {
@@ -173,9 +187,15 @@ pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides)
}
/// Print the user-facing configuration settings.
pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<()> {
pub fn show_settings(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<()> {
// Collect all files in the hierarchy.
let (paths, resolver) = resolver::python_files_in_path(files, strategy, overrides)?;
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
// Print the list of files.
let Some(entry) = paths
@@ -185,7 +205,7 @@ pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrid
bail!("No files found under the given path");
};
let path = entry.path();
let settings = resolver.resolve(path, strategy);
let settings = resolver.resolve(path, pyproject_strategy);
println!("Resolved settings for: {path:?}");
println!("{settings:#?}");
@@ -193,9 +213,15 @@ pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrid
}
/// Show the list of files to be checked based on current settings.
pub fn show_files(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<()> {
pub fn show_files(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<()> {
// Collect all files in the hierarchy.
let (paths, _resolver) = resolver::python_files_in_path(files, strategy, overrides)?;
let (paths, _resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
// Print the list of files.
for entry in paths

View File

@@ -20,7 +20,7 @@ use ::ruff::autofix::fixer;
use ::ruff::cli::{extract_log_level, Cli};
use ::ruff::logging::{set_up_logging, LogLevel};
use ::ruff::printer::Printer;
use ::ruff::resolver::Strategy;
use ::ruff::resolver::PyprojectDiscovery;
use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::types::SerializationFormat;
use ::ruff::settings::{pyproject, Settings};
@@ -33,31 +33,31 @@ use colored::Colorize;
use notify::{recommended_watcher, RecursiveMode, Watcher};
use path_absolutize::path_dedot;
use ruff::cli::Overrides;
use ruff::resolver::{resolve_settings, Relativity};
use ruff::resolver::{resolve_settings, FileDiscovery, Relativity};
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<Strategy> {
fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<PyprojectDiscovery> {
if let Some(pyproject) = config {
// First priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
Ok(Strategy::Fixed(settings))
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = pyproject::find_pyproject_toml(path_dedot::CWD.as_path()) {
// Second priority: find a `pyproject.toml` file in the current working path,
// and resolve all paths relative to that directory. (With
// `Strategy::Hierarchical`, we'll end up finding the "closest" `pyproject.toml`
// file for every Python file later on, so these act as the "default" settings.)
let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
Ok(Strategy::Hierarchical(settings))
Ok(PyprojectDiscovery::Hierarchical(settings))
} else if let Some(pyproject) = pyproject::find_user_pyproject_toml() {
// Third priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
Ok(Strategy::Hierarchical(settings))
Ok(PyprojectDiscovery::Hierarchical(settings))
} else {
// Fallback: load Ruff's default settings, and resolve all paths relative to the
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
@@ -67,7 +67,7 @@ fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<Strategy> {
// Apply command-line options that override defaults.
config.apply(overrides.clone());
let settings = Settings::from_configuration(config, &path_dedot::CWD)?;
Ok(Strategy::Hierarchical(settings))
Ok(PyprojectDiscovery::Hierarchical(settings))
}
}
@@ -87,13 +87,19 @@ fn inner_main() -> Result<ExitCode> {
// Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy.
let strategy = resolve(cli.config, &overrides)?;
let pyproject_strategy = resolve(cli.config, &overrides)?;
// Extract options that are included in `Settings`, but only apply at the top
// level.
let (fix, format) = match &strategy {
Strategy::Fixed(settings) => (settings.fix, settings.format),
Strategy::Hierarchical(settings) => (settings.fix, settings.format),
let file_strategy = FileDiscovery {
respect_gitignore: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.respect_gitignore,
PyprojectDiscovery::Hierarchical(settings) => settings.respect_gitignore,
},
};
let (fix, format) = match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => (settings.fix, settings.format),
PyprojectDiscovery::Hierarchical(settings) => (settings.fix, settings.format),
};
let autofix = if fix {
fixer::Mode::Apply
@@ -108,11 +114,11 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {
commands::show_settings(&cli.files, &strategy, &overrides)?;
commands::show_settings(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
commands::show_files(&cli.files, &strategy, &overrides)?;
commands::show_files(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
@@ -144,7 +150,8 @@ fn inner_main() -> Result<ExitCode> {
let messages = commands::run(
&cli.files,
&strategy,
&pyproject_strategy,
&file_strategy,
&overrides,
cache_enabled,
&fixer::Mode::None,
@@ -173,7 +180,8 @@ fn inner_main() -> Result<ExitCode> {
let messages = commands::run(
&cli.files,
&strategy,
&pyproject_strategy,
&file_strategy,
&overrides,
cache_enabled,
&fixer::Mode::None,
@@ -185,12 +193,14 @@ fn inner_main() -> Result<ExitCode> {
}
}
} else if cli.add_noqa {
let modifications = commands::add_noqa(&cli.files, &strategy, &overrides)?;
let modifications =
commands::add_noqa(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives.");
}
} else if cli.autoformat {
let modifications = commands::autoformat(&cli.files, &strategy, &overrides)?;
let modifications =
commands::autoformat(&cli.files, &pyproject_strategy, &file_strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Formatted {modifications} files.");
}
@@ -201,9 +211,16 @@ fn inner_main() -> Result<ExitCode> {
let diagnostics = if is_stdin {
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename);
commands::run_stdin(&strategy, path, &autofix)?
commands::run_stdin(&pyproject_strategy, path, &autofix)?
} else {
commands::run(&cli.files, &strategy, &overrides, cache_enabled, &autofix)?
commands::run(
&cli.files,
&pyproject_strategy,
&file_strategy,
&overrides,
cache_enabled,
&autofix,
)?
};
// Always try to print violations (the printer itself may suppress output),

View File

@@ -16,9 +16,16 @@ use crate::fs;
use crate::settings::configuration::Configuration;
use crate::settings::{pyproject, Settings};
/// The strategy for discovering a `pyproject.toml` file for each Python file.
/// The strategy used to discover Python files in the filesystem..
#[derive(Debug)]
pub enum Strategy {
pub struct FileDiscovery {
pub respect_gitignore: bool,
}
/// The strategy used to discover the relevant `pyproject.toml` file for each
/// Python file.
#[derive(Debug)]
pub enum PyprojectDiscovery {
/// Use a fixed `pyproject.toml` file for all Python files (i.e., one
/// provided on the command-line).
Fixed(Settings),
@@ -65,10 +72,10 @@ impl Resolver {
}
/// Return the appropriate `Settings` for a given `Path`.
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a Strategy) -> &'a Settings {
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a PyprojectDiscovery) -> &'a Settings {
match strategy {
Strategy::Fixed(settings) => settings,
Strategy::Hierarchical(default) => self
PyprojectDiscovery::Fixed(settings) => settings,
PyprojectDiscovery::Hierarchical(default) => self
.settings
.iter()
.rev()
@@ -181,7 +188,8 @@ fn is_python_entry(entry: &DirEntry) -> bool {
/// Find all Python (`.py` and `.pyi` files) in a set of paths.
pub fn python_files_in_path(
paths: &[PathBuf],
strategy: &Strategy,
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
// Normalize every path (e.g., convert from relative to absolute).
@@ -209,7 +217,9 @@ pub fn python_files_in_path(
for path in &paths[1..] {
builder.add(path);
}
let walker = builder.hidden(false).build_parallel();
builder.standard_filters(file_strategy.respect_gitignore);
builder.hidden(false);
let walker = builder.build_parallel();
// Run the `WalkParallel` to collect all Python files.
let error: std::sync::Mutex<Result<()>> = std::sync::Mutex::new(Ok(()));
@@ -247,7 +257,7 @@ pub fn python_files_in_path(
if entry.depth() > 0 {
let path = entry.path();
let resolver = resolver.read().unwrap();
let settings = resolver.resolve(path, strategy);
let settings = resolver.resolve(path, pyproject_strategy);
match fs::extract_path_names(path) {
Ok((file_path, file_basename)) => {
if !settings.exclude.is_empty()

View File

@@ -35,6 +35,7 @@ pub struct Configuration {
pub ignore_init_module_imports: Option<bool>,
pub line_length: Option<usize>,
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
pub respect_gitignore: Option<bool>,
pub select: Option<Vec<CheckCodePrefix>>,
pub show_source: Option<bool>,
pub src: Option<Vec<PathBuf>>,
@@ -109,6 +110,7 @@ impl Configuration {
})
.collect()
}),
respect_gitignore: options.respect_gitignore,
show_source: options.show_source,
// Plugins
flake8_annotations: options.flake8_annotations,
@@ -129,6 +131,7 @@ impl Configuration {
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
exclude: self.exclude.or(config.exclude),
respect_gitignore: self.respect_gitignore.or(config.respect_gitignore),
extend: self.extend.or(config.extend),
extend_exclude: self.extend_exclude.or(config.extend_exclude),
extend_ignore: self.extend_ignore.or(config.extend_ignore),
@@ -202,6 +205,9 @@ impl Configuration {
if let Some(per_file_ignores) = overrides.per_file_ignores {
self.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores));
}
if let Some(respect_gitignore) = overrides.respect_gitignore {
self.respect_gitignore = Some(respect_gitignore);
}
if let Some(select) = overrides.select {
self.select = Some(select);
}

View File

@@ -29,6 +29,7 @@ pub mod pyproject;
pub mod types;
#[derive(Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct Settings {
pub allowed_confusables: FxHashSet<char>,
pub dummy_variable_rgx: Regex,
@@ -42,6 +43,7 @@ pub struct Settings {
pub ignore_init_module_imports: bool,
pub line_length: usize,
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)>,
pub respect_gitignore: bool,
pub show_source: bool,
pub src: Vec<PathBuf>,
pub target_version: PythonVersion,
@@ -122,6 +124,7 @@ impl Settings {
per_file_ignores: resolve_per_file_ignores(
config.per_file_ignores.unwrap_or_default(),
)?,
respect_gitignore: config.respect_gitignore.unwrap_or(true),
src: config
.src
.unwrap_or_else(|| vec![project_root.to_path_buf()]),
@@ -183,6 +186,7 @@ impl Settings {
ignore_init_module_imports: false,
line_length: 88,
per_file_ignores: vec![],
respect_gitignore: true,
show_source: false,
src: vec![path_dedot::CWD.clone()],
target_version: PythonVersion::Py310,
@@ -212,6 +216,7 @@ impl Settings {
ignore_init_module_imports: false,
line_length: 88,
per_file_ignores: vec![],
respect_gitignore: true,
show_source: false,
src: vec![path_dedot::CWD.clone()],
target_version: PythonVersion::Py310,

View File

@@ -205,6 +205,18 @@ pub struct Options {
"#
)]
pub line_length: Option<usize>,
#[option(
doc = r#"
Whether to automatically exclude files that are ignored by `.ignore`, `.gitignore`,
`.git/info/exclude`, and global `gitignore` files. Enabled by default.
"#,
default = "true",
value_type = "bool",
example = r#"
respect_gitignore = false
"#
)]
pub respect_gitignore: Option<bool>,
#[option(
doc = r#"
A list of check code prefixes to enable. Prefixes can specify exact checks (like

View File

@@ -119,6 +119,7 @@ mod tests {
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: None,
show_source: None,
src: None,
@@ -163,6 +164,7 @@ line-length = 79
ignore_init_module_imports: None,
line_length: Some(79),
per_file_ignores: None,
respect_gitignore: None,
select: None,
show_source: None,
src: None,
@@ -209,6 +211,7 @@ exclude = ["foo.py"]
format: None,
unfixable: None,
per_file_ignores: None,
respect_gitignore: None,
dummy_variable_rgx: None,
src: None,
target_version: None,
@@ -251,6 +254,7 @@ select = ["E501"]
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: Some(vec![CheckCodePrefix::E501]),
show_source: None,
src: None,
@@ -296,6 +300,7 @@ ignore = ["E501"]
ignore_init_module_imports: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
select: None,
show_source: None,
src: None,
@@ -383,8 +388,9 @@ other-attribute = 1
per_file_ignores: Some(FxHashMap::from_iter([(
"__init__.py".to_string(),
vec![CheckCodePrefix::F401]
),])),
)])),
dummy_variable_rgx: None,
respect_gitignore: None,
src: None,
target_version: None,
show_source: None,