mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 02:12:22 +00:00
Add [format|lint].exclude
options (#8000)
This commit is contained in:
parent
d685107638
commit
fe485d791c
22 changed files with 772 additions and 294 deletions
|
@ -17,8 +17,8 @@ use ruff_linter::settings::DEFAULT_SELECTORS;
|
||||||
use ruff_linter::warn_user;
|
use ruff_linter::warn_user;
|
||||||
use ruff_workspace::options::{
|
use ruff_workspace::options::{
|
||||||
Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
|
Flake8AnnotationsOptions, Flake8BugbearOptions, Flake8BuiltinsOptions, Flake8ErrMsgOptions,
|
||||||
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintOptions,
|
Flake8PytestStyleOptions, Flake8QuotesOptions, Flake8TidyImportsOptions, LintCommonOptions,
|
||||||
McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
|
LintOptions, McCabeOptions, Options, Pep8NamingOptions, PydocstyleOptions,
|
||||||
};
|
};
|
||||||
use ruff_workspace::pyproject::Pyproject;
|
use ruff_workspace::pyproject::Pyproject;
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ pub(crate) fn convert(
|
||||||
|
|
||||||
// Parse each supported option.
|
// Parse each supported option.
|
||||||
let mut options = Options::default();
|
let mut options = Options::default();
|
||||||
let mut lint_options = LintOptions::default();
|
let mut lint_options = LintCommonOptions::default();
|
||||||
let mut flake8_annotations = Flake8AnnotationsOptions::default();
|
let mut flake8_annotations = Flake8AnnotationsOptions::default();
|
||||||
let mut flake8_bugbear = Flake8BugbearOptions::default();
|
let mut flake8_bugbear = Flake8BugbearOptions::default();
|
||||||
let mut flake8_builtins = Flake8BuiltinsOptions::default();
|
let mut flake8_builtins = Flake8BuiltinsOptions::default();
|
||||||
|
@ -433,8 +433,11 @@ pub(crate) fn convert(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if lint_options != LintOptions::default() {
|
if lint_options != LintCommonOptions::default() {
|
||||||
options.lint = Some(lint_options);
|
options.lint = Some(LintOptions {
|
||||||
|
common: lint_options,
|
||||||
|
..LintOptions::default()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the pyproject.toml.
|
// Create the pyproject.toml.
|
||||||
|
@ -465,7 +468,9 @@ mod tests {
|
||||||
use ruff_linter::rules::flake8_quotes;
|
use ruff_linter::rules::flake8_quotes;
|
||||||
use ruff_linter::rules::pydocstyle::settings::Convention;
|
use ruff_linter::rules::pydocstyle::settings::Convention;
|
||||||
use ruff_linter::settings::types::PythonVersion;
|
use ruff_linter::settings::types::PythonVersion;
|
||||||
use ruff_workspace::options::{Flake8QuotesOptions, LintOptions, Options, PydocstyleOptions};
|
use ruff_workspace::options::{
|
||||||
|
Flake8QuotesOptions, LintCommonOptions, LintOptions, Options, PydocstyleOptions,
|
||||||
|
};
|
||||||
use ruff_workspace::pyproject::Pyproject;
|
use ruff_workspace::pyproject::Pyproject;
|
||||||
|
|
||||||
use crate::converter::DEFAULT_SELECTORS;
|
use crate::converter::DEFAULT_SELECTORS;
|
||||||
|
@ -475,8 +480,8 @@ mod tests {
|
||||||
use super::super::plugin::Plugin;
|
use super::super::plugin::Plugin;
|
||||||
use super::convert;
|
use super::convert;
|
||||||
|
|
||||||
fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintOptions {
|
fn lint_default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> LintCommonOptions {
|
||||||
LintOptions {
|
LintCommonOptions {
|
||||||
ignore: Some(vec![]),
|
ignore: Some(vec![]),
|
||||||
select: Some(
|
select: Some(
|
||||||
DEFAULT_SELECTORS
|
DEFAULT_SELECTORS
|
||||||
|
@ -486,7 +491,7 @@ mod tests {
|
||||||
.sorted_by_key(RuleSelector::prefix_and_code)
|
.sorted_by_key(RuleSelector::prefix_and_code)
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
..LintOptions::default()
|
..LintCommonOptions::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,7 +503,10 @@ mod tests {
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
let expected = Pyproject::new(Options {
|
let expected = Pyproject::new(Options {
|
||||||
lint: Some(lint_default_options([])),
|
lint: Some(LintOptions {
|
||||||
|
common: lint_default_options([]),
|
||||||
|
..LintOptions::default()
|
||||||
|
}),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
});
|
});
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
|
@ -516,7 +524,10 @@ mod tests {
|
||||||
);
|
);
|
||||||
let expected = Pyproject::new(Options {
|
let expected = Pyproject::new(Options {
|
||||||
line_length: Some(LineLength::try_from(100).unwrap()),
|
line_length: Some(LineLength::try_from(100).unwrap()),
|
||||||
lint: Some(lint_default_options([])),
|
lint: Some(LintOptions {
|
||||||
|
common: lint_default_options([]),
|
||||||
|
..LintOptions::default()
|
||||||
|
}),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
});
|
});
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
|
@ -534,7 +545,10 @@ mod tests {
|
||||||
);
|
);
|
||||||
let expected = Pyproject::new(Options {
|
let expected = Pyproject::new(Options {
|
||||||
line_length: Some(LineLength::try_from(100).unwrap()),
|
line_length: Some(LineLength::try_from(100).unwrap()),
|
||||||
lint: Some(lint_default_options([])),
|
lint: Some(LintOptions {
|
||||||
|
common: lint_default_options([]),
|
||||||
|
..LintOptions::default()
|
||||||
|
}),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
});
|
});
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
|
@ -551,7 +565,10 @@ mod tests {
|
||||||
Some(vec![]),
|
Some(vec![]),
|
||||||
);
|
);
|
||||||
let expected = Pyproject::new(Options {
|
let expected = Pyproject::new(Options {
|
||||||
lint: Some(lint_default_options([])),
|
lint: Some(LintOptions {
|
||||||
|
common: lint_default_options([]),
|
||||||
|
..LintOptions::default()
|
||||||
|
}),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
});
|
});
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
|
@ -569,6 +586,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
let expected = Pyproject::new(Options {
|
let expected = Pyproject::new(Options {
|
||||||
lint: Some(LintOptions {
|
lint: Some(LintOptions {
|
||||||
|
common: LintCommonOptions {
|
||||||
flake8_quotes: Some(Flake8QuotesOptions {
|
flake8_quotes: Some(Flake8QuotesOptions {
|
||||||
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
|
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
|
||||||
multiline_quotes: None,
|
multiline_quotes: None,
|
||||||
|
@ -576,6 +594,8 @@ mod tests {
|
||||||
avoid_escape: None,
|
avoid_escape: None,
|
||||||
}),
|
}),
|
||||||
..lint_default_options([])
|
..lint_default_options([])
|
||||||
|
},
|
||||||
|
..LintOptions::default()
|
||||||
}),
|
}),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
});
|
});
|
||||||
|
@ -597,12 +617,15 @@ mod tests {
|
||||||
);
|
);
|
||||||
let expected = Pyproject::new(Options {
|
let expected = Pyproject::new(Options {
|
||||||
lint: Some(LintOptions {
|
lint: Some(LintOptions {
|
||||||
|
common: LintCommonOptions {
|
||||||
pydocstyle: Some(PydocstyleOptions {
|
pydocstyle: Some(PydocstyleOptions {
|
||||||
convention: Some(Convention::Numpy),
|
convention: Some(Convention::Numpy),
|
||||||
ignore_decorators: None,
|
ignore_decorators: None,
|
||||||
property_decorators: None,
|
property_decorators: None,
|
||||||
}),
|
}),
|
||||||
..lint_default_options([Linter::Pydocstyle.into()])
|
..lint_default_options([Linter::Pydocstyle.into()])
|
||||||
|
},
|
||||||
|
..LintOptions::default()
|
||||||
}),
|
}),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
});
|
});
|
||||||
|
@ -621,6 +644,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
let expected = Pyproject::new(Options {
|
let expected = Pyproject::new(Options {
|
||||||
lint: Some(LintOptions {
|
lint: Some(LintOptions {
|
||||||
|
common: LintCommonOptions {
|
||||||
flake8_quotes: Some(Flake8QuotesOptions {
|
flake8_quotes: Some(Flake8QuotesOptions {
|
||||||
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
|
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
|
||||||
multiline_quotes: None,
|
multiline_quotes: None,
|
||||||
|
@ -628,6 +652,8 @@ mod tests {
|
||||||
avoid_escape: None,
|
avoid_escape: None,
|
||||||
}),
|
}),
|
||||||
..lint_default_options([Linter::Flake8Quotes.into()])
|
..lint_default_options([Linter::Flake8Quotes.into()])
|
||||||
|
},
|
||||||
|
..LintOptions::default()
|
||||||
}),
|
}),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
});
|
});
|
||||||
|
@ -648,7 +674,10 @@ mod tests {
|
||||||
);
|
);
|
||||||
let expected = Pyproject::new(Options {
|
let expected = Pyproject::new(Options {
|
||||||
target_version: Some(PythonVersion::Py38),
|
target_version: Some(PythonVersion::Py38),
|
||||||
lint: Some(lint_default_options([])),
|
lint: Some(LintOptions {
|
||||||
|
common: lint_default_options([]),
|
||||||
|
..LintOptions::default()
|
||||||
|
}),
|
||||||
..Options::default()
|
..Options::default()
|
||||||
});
|
});
|
||||||
assert_eq!(actual, expected);
|
assert_eq!(actual, expected);
|
||||||
|
|
|
@ -366,6 +366,15 @@ pub struct FormatCommand {
|
||||||
respect_gitignore: bool,
|
respect_gitignore: bool,
|
||||||
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
|
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
|
||||||
no_respect_gitignore: bool,
|
no_respect_gitignore: bool,
|
||||||
|
/// List of paths, used to omit files and/or directories from analysis.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
value_delimiter = ',',
|
||||||
|
value_name = "FILE_PATTERN",
|
||||||
|
help_heading = "File selection"
|
||||||
|
)]
|
||||||
|
pub exclude: Option<Vec<FilePattern>>,
|
||||||
|
|
||||||
/// Enforce exclusions, even for paths passed to Ruff directly on the command-line.
|
/// Enforce exclusions, even for paths passed to Ruff directly on the command-line.
|
||||||
/// Use `--no-force-exclude` to disable.
|
/// Use `--no-force-exclude` to disable.
|
||||||
#[arg(
|
#[arg(
|
||||||
|
@ -522,6 +531,7 @@ impl FormatCommand {
|
||||||
self.respect_gitignore,
|
self.respect_gitignore,
|
||||||
self.no_respect_gitignore,
|
self.no_respect_gitignore,
|
||||||
),
|
),
|
||||||
|
exclude: self.exclude,
|
||||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||||
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
|
||||||
// Unsupported on the formatter CLI, but required on `Overrides`.
|
// Unsupported on the formatter CLI, but required on `Overrides`.
|
||||||
|
|
|
@ -10,7 +10,7 @@ use ruff_linter::linter::add_noqa_to_path;
|
||||||
use ruff_linter::source_kind::SourceKind;
|
use ruff_linter::source_kind::SourceKind;
|
||||||
use ruff_linter::warn_user_once;
|
use ruff_linter::warn_user_once;
|
||||||
use ruff_python_ast::{PySourceType, SourceType};
|
use ruff_python_ast::{PySourceType, SourceType};
|
||||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
|
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||||
|
|
||||||
use crate::args::CliOverrides;
|
use crate::args::CliOverrides;
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ pub(crate) fn add_noqa(
|
||||||
&paths
|
&paths
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(ignore::DirEntry::path)
|
.map(ResolvedFile::path)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
pyproject_config,
|
pyproject_config,
|
||||||
);
|
);
|
||||||
|
@ -45,14 +45,15 @@ pub(crate) fn add_noqa(
|
||||||
let modifications: usize = paths
|
let modifications: usize = paths
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(|entry| {
|
.filter_map(|resolved_file| {
|
||||||
let path = entry.path();
|
|
||||||
let SourceType::Python(source_type @ (PySourceType::Python | PySourceType::Stub)) =
|
let SourceType::Python(source_type @ (PySourceType::Python | PySourceType::Stub)) =
|
||||||
SourceType::from(path)
|
SourceType::from(resolved_file.path())
|
||||||
else {
|
else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let package = path
|
let path = resolved_file.path();
|
||||||
|
let package = resolved_file
|
||||||
|
.path()
|
||||||
.parent()
|
.parent()
|
||||||
.and_then(|parent| package_roots.get(parent))
|
.and_then(|parent| package_roots.get(parent))
|
||||||
.and_then(|package| *package);
|
.and_then(|package| *package);
|
||||||
|
|
|
@ -22,7 +22,10 @@ use ruff_linter::{fs, warn_user_once, IOError};
|
||||||
use ruff_python_ast::imports::ImportMap;
|
use ruff_python_ast::imports::ImportMap;
|
||||||
use ruff_source_file::SourceFileBuilder;
|
use ruff_source_file::SourceFileBuilder;
|
||||||
use ruff_text_size::{TextRange, TextSize};
|
use ruff_text_size::{TextRange, TextSize};
|
||||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, PyprojectDiscoveryStrategy};
|
use ruff_workspace::resolver::{
|
||||||
|
match_exclusion, python_files_in_path, PyprojectConfig, PyprojectDiscoveryStrategy,
|
||||||
|
ResolvedFile,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::args::CliOverrides;
|
use crate::args::CliOverrides;
|
||||||
use crate::cache::{self, Cache};
|
use crate::cache::{self, Cache};
|
||||||
|
@ -42,8 +45,7 @@ pub(crate) fn check(
|
||||||
// Collect all the Python files to check.
|
// Collect all the Python files to check.
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
||||||
let duration = start.elapsed();
|
debug!("Identified files to lint in: {:?}", start.elapsed());
|
||||||
debug!("Identified files to lint in: {:?}", duration);
|
|
||||||
|
|
||||||
if paths.is_empty() {
|
if paths.is_empty() {
|
||||||
warn_user_once!("No Python files found under the given path(s)");
|
warn_user_once!("No Python files found under the given path(s)");
|
||||||
|
@ -77,7 +79,7 @@ pub(crate) fn check(
|
||||||
&paths
|
&paths
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(ignore::DirEntry::path)
|
.map(ResolvedFile::path)
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
pyproject_config,
|
pyproject_config,
|
||||||
);
|
);
|
||||||
|
@ -98,12 +100,10 @@ pub(crate) fn check(
|
||||||
});
|
});
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut diagnostics: Diagnostics = paths
|
let diagnostics_per_file = paths.par_iter().filter_map(|resolved_file| {
|
||||||
.par_iter()
|
let result = match resolved_file {
|
||||||
.map(|entry| {
|
Ok(resolved_file) => {
|
||||||
match entry {
|
let path = resolved_file.path();
|
||||||
Ok(entry) => {
|
|
||||||
let path = entry.path();
|
|
||||||
let package = path
|
let package = path
|
||||||
.parent()
|
.parent()
|
||||||
.and_then(|parent| package_roots.get(parent))
|
.and_then(|parent| package_roots.get(parent))
|
||||||
|
@ -111,6 +111,16 @@ pub(crate) fn check(
|
||||||
|
|
||||||
let settings = resolver.resolve(path, pyproject_config);
|
let settings = resolver.resolve(path, pyproject_config);
|
||||||
|
|
||||||
|
if !resolved_file.is_root()
|
||||||
|
&& match_exclusion(
|
||||||
|
resolved_file.path(),
|
||||||
|
resolved_file.file_name(),
|
||||||
|
&settings.linter.exclude,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path));
|
let cache_root = package.unwrap_or_else(|| path.parent().unwrap_or(path));
|
||||||
let cache = caches.as_ref().and_then(|caches| {
|
let cache = caches.as_ref().and_then(|caches| {
|
||||||
if let Some(cache) = caches.get(&cache_root) {
|
if let Some(cache) = caches.get(&cache_root) {
|
||||||
|
@ -131,7 +141,7 @@ pub(crate) fn check(
|
||||||
unsafe_fixes,
|
unsafe_fixes,
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
(Some(path.to_owned()), {
|
(Some(path.to_path_buf()), {
|
||||||
let mut error = e.to_string();
|
let mut error = e.to_string();
|
||||||
for cause in e.chain() {
|
for cause in e.chain() {
|
||||||
write!(&mut error, "\n Cause: {cause}").unwrap();
|
write!(&mut error, "\n Cause: {cause}").unwrap();
|
||||||
|
@ -149,8 +159,9 @@ pub(crate) fn check(
|
||||||
e.io_error()
|
e.io_error()
|
||||||
.map_or_else(|| e.to_string(), io::Error::to_string),
|
.map_or_else(|| e.to_string(), io::Error::to_string),
|
||||||
)),
|
)),
|
||||||
}
|
};
|
||||||
.unwrap_or_else(|(path, message)| {
|
|
||||||
|
Some(result.unwrap_or_else(|(path, message)| {
|
||||||
if let Some(path) = &path {
|
if let Some(path) = &path {
|
||||||
let settings = resolver.resolve(path, pyproject_config);
|
let settings = resolver.resolve(path, pyproject_config);
|
||||||
if settings.linter.rules.enabled(Rule::IOError) {
|
if settings.linter.rules.enabled(Rule::IOError) {
|
||||||
|
@ -179,14 +190,24 @@ pub(crate) fn check(
|
||||||
warn!("{} {message}", "Encountered error:".bold());
|
warn!("{} {message}", "Encountered error:".bold());
|
||||||
Diagnostics::default()
|
Diagnostics::default()
|
||||||
}
|
}
|
||||||
})
|
}))
|
||||||
})
|
|
||||||
.reduce(Diagnostics::default, |mut acc, item| {
|
|
||||||
acc += item;
|
|
||||||
acc
|
|
||||||
});
|
});
|
||||||
|
|
||||||
diagnostics.messages.sort();
|
// Aggregate the diagnostics of all checked files and count the checked files.
|
||||||
|
// This can't be a regular for loop because we use `par_iter`.
|
||||||
|
let (mut all_diagnostics, checked_files) = diagnostics_per_file
|
||||||
|
.fold(
|
||||||
|
|| (Diagnostics::default(), 0u64),
|
||||||
|
|(all_diagnostics, checked_files), file_diagnostics| {
|
||||||
|
(all_diagnostics + file_diagnostics, checked_files + 1)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
|| (Diagnostics::default(), 0u64),
|
||||||
|
|a, b| (a.0 + b.0, a.1 + b.1),
|
||||||
|
);
|
||||||
|
|
||||||
|
all_diagnostics.messages.sort();
|
||||||
|
|
||||||
// Store the caches.
|
// Store the caches.
|
||||||
if let Some(caches) = caches {
|
if let Some(caches) = caches {
|
||||||
|
@ -196,9 +217,9 @@ pub(crate) fn check(
|
||||||
}
|
}
|
||||||
|
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
debug!("Checked {:?} files in: {:?}", paths.len(), duration);
|
debug!("Checked {:?} files in: {:?}", checked_files, duration);
|
||||||
|
|
||||||
Ok(diagnostics)
|
Ok(all_diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits
|
/// Wraps [`lint_path`](crate::diagnostics::lint_path) in a [`catch_unwind`](std::panic::catch_unwind) and emits
|
||||||
|
|
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
||||||
|
|
||||||
use ruff_linter::packaging;
|
use ruff_linter::packaging;
|
||||||
use ruff_linter::settings::flags;
|
use ruff_linter::settings::flags;
|
||||||
use ruff_workspace::resolver::{python_file_at_path, PyprojectConfig};
|
use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig};
|
||||||
|
|
||||||
use crate::args::CliOverrides;
|
use crate::args::CliOverrides;
|
||||||
use crate::diagnostics::{lint_stdin, Diagnostics};
|
use crate::diagnostics::{lint_stdin, Diagnostics};
|
||||||
|
@ -22,6 +22,14 @@ pub(crate) fn check_stdin(
|
||||||
if !python_file_at_path(filename, pyproject_config, overrides)? {
|
if !python_file_at_path(filename, pyproject_config, overrides)? {
|
||||||
return Ok(Diagnostics::default());
|
return Ok(Diagnostics::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lint_settings = &pyproject_config.settings.linter;
|
||||||
|
if filename
|
||||||
|
.file_name()
|
||||||
|
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
|
||||||
|
{
|
||||||
|
return Ok(Diagnostics::default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let package_root = filename.and_then(Path::parent).and_then(|path| {
|
let package_root = filename.and_then(Path::parent).and_then(|path| {
|
||||||
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
|
packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages)
|
||||||
|
|
|
@ -20,7 +20,7 @@ use ruff_linter::warn_user_once;
|
||||||
use ruff_python_ast::{PySourceType, SourceType};
|
use ruff_python_ast::{PySourceType, SourceType};
|
||||||
use ruff_python_formatter::{format_module_source, FormatModuleError};
|
use ruff_python_formatter::{format_module_source, FormatModuleError};
|
||||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||||
use ruff_workspace::resolver::python_files_in_path;
|
use ruff_workspace::resolver::{match_exclusion, python_files_in_path};
|
||||||
use ruff_workspace::FormatterSettings;
|
use ruff_workspace::FormatterSettings;
|
||||||
|
|
||||||
use crate::args::{CliOverrides, FormatArguments};
|
use crate::args::{CliOverrides, FormatArguments};
|
||||||
|
@ -61,26 +61,42 @@ pub(crate) fn format(
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let (results, errors): (Vec<_>, Vec<_>) = paths
|
let (mut results, errors): (Vec<_>, Vec<_>) = paths
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter_map(|entry| {
|
.filter_map(|entry| {
|
||||||
match entry {
|
match entry {
|
||||||
Ok(entry) => {
|
Ok(resolved_file) => {
|
||||||
let path = entry.into_path();
|
let path = resolved_file.path();
|
||||||
|
|
||||||
let SourceType::Python(source_type) = SourceType::from(&path) else {
|
let SourceType::Python(source_type) = SourceType::from(&path) else {
|
||||||
// Ignore any non-Python files.
|
// Ignore any non-Python files.
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let resolved_settings = resolver.resolve(&path, &pyproject_config);
|
let resolved_settings = resolver.resolve(path, &pyproject_config);
|
||||||
|
|
||||||
|
// Ignore files that are excluded from formatting
|
||||||
|
if !resolved_file.is_root()
|
||||||
|
&& match_exclusion(
|
||||||
|
path,
|
||||||
|
resolved_file.file_name(),
|
||||||
|
&resolved_settings.formatter.exclude,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
match catch_unwind(|| {
|
match catch_unwind(|| {
|
||||||
format_path(&path, &resolved_settings.formatter, source_type, mode)
|
format_path(path, &resolved_settings.formatter, source_type, mode)
|
||||||
}) {
|
}) {
|
||||||
Ok(inner) => inner.map(|result| FormatPathResult { path, result }),
|
Ok(inner) => inner.map(|result| FormatPathResult {
|
||||||
Err(error) => Err(FormatCommandError::Panic(Some(path), error)),
|
path: resolved_file.into_path(),
|
||||||
|
result,
|
||||||
|
}),
|
||||||
|
Err(error) => Err(FormatCommandError::Panic(
|
||||||
|
Some(resolved_file.into_path()),
|
||||||
|
error,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -104,6 +120,8 @@ pub(crate) fn format(
|
||||||
error!("{error}");
|
error!("{error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
results.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||||
|
|
||||||
let summary = FormatSummary::new(results.as_slice(), mode);
|
let summary = FormatSummary::new(results.as_slice(), mode);
|
||||||
|
|
||||||
// Report on the formatting changes.
|
// Report on the formatting changes.
|
||||||
|
@ -137,7 +155,7 @@ pub(crate) fn format(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format the file at the given [`Path`].
|
/// Format the file at the given [`Path`].
|
||||||
#[tracing::instrument(skip_all, fields(path = %path.display()))]
|
#[tracing::instrument(level="debug", skip_all, fields(path = %path.display()))]
|
||||||
fn format_path(
|
fn format_path(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
settings: &FormatterSettings,
|
settings: &FormatterSettings,
|
||||||
|
|
|
@ -6,7 +6,7 @@ use log::error;
|
||||||
|
|
||||||
use ruff_linter::source_kind::SourceKind;
|
use ruff_linter::source_kind::SourceKind;
|
||||||
use ruff_python_ast::{PySourceType, SourceType};
|
use ruff_python_ast::{PySourceType, SourceType};
|
||||||
use ruff_workspace::resolver::python_file_at_path;
|
use ruff_workspace::resolver::{match_exclusion, python_file_at_path};
|
||||||
use ruff_workspace::FormatterSettings;
|
use ruff_workspace::FormatterSettings;
|
||||||
|
|
||||||
use crate::args::{CliOverrides, FormatArguments};
|
use crate::args::{CliOverrides, FormatArguments};
|
||||||
|
@ -33,6 +33,14 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
|
||||||
if !python_file_at_path(filename, &pyproject_config, overrides)? {
|
if !python_file_at_path(filename, &pyproject_config, overrides)? {
|
||||||
return Ok(ExitStatus::Success);
|
return Ok(ExitStatus::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let format_settings = &pyproject_config.settings.formatter;
|
||||||
|
if filename
|
||||||
|
.file_name()
|
||||||
|
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
|
||||||
|
{
|
||||||
|
return Ok(ExitStatus::Success);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = cli.stdin_filename.as_deref();
|
let path = cli.stdin_filename.as_deref();
|
||||||
|
|
|
@ -5,7 +5,7 @@ use anyhow::Result;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use ruff_linter::warn_user_once;
|
use ruff_linter::warn_user_once;
|
||||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
|
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||||
|
|
||||||
use crate::args::CliOverrides;
|
use crate::args::CliOverrides;
|
||||||
|
|
||||||
|
@ -25,12 +25,13 @@ pub(crate) fn show_files(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print the list of files.
|
// Print the list of files.
|
||||||
for entry in paths
|
for path in paths
|
||||||
.iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.sorted_by(|a, b| a.path().cmp(b.path()))
|
.map(ResolvedFile::into_path)
|
||||||
|
.sorted_unstable()
|
||||||
{
|
{
|
||||||
writeln!(writer, "{}", entry.path().to_string_lossy())?;
|
writeln!(writer, "{}", path.to_string_lossy())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -4,7 +4,7 @@ use std::path::PathBuf;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig};
|
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile};
|
||||||
|
|
||||||
use crate::args::CliOverrides;
|
use crate::args::CliOverrides;
|
||||||
|
|
||||||
|
@ -19,16 +19,17 @@ pub(crate) fn show_settings(
|
||||||
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
let (paths, resolver) = python_files_in_path(files, pyproject_config, overrides)?;
|
||||||
|
|
||||||
// Print the list of files.
|
// Print the list of files.
|
||||||
let Some(entry) = paths
|
let Some(path) = paths
|
||||||
.iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.sorted_by(|a, b| a.path().cmp(b.path()))
|
.map(ResolvedFile::into_path)
|
||||||
|
.sorted_unstable()
|
||||||
.next()
|
.next()
|
||||||
else {
|
else {
|
||||||
bail!("No files found under the given path");
|
bail!("No files found under the given path");
|
||||||
};
|
};
|
||||||
let path = entry.path();
|
|
||||||
let settings = resolver.resolve(path, pyproject_config);
|
let settings = resolver.resolve(&path, pyproject_config);
|
||||||
|
|
||||||
writeln!(writer, "Resolved settings for: {path:?}")?;
|
writeln!(writer, "Resolved settings for: {path:?}")?;
|
||||||
if let Some(settings_path) = pyproject_config.path.as_ref() {
|
if let Some(settings_path) = pyproject_config.path.as_ref() {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::ops::AddAssign;
|
use std::ops::{Add, AddAssign};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -142,6 +142,15 @@ impl Diagnostics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Add for Diagnostics {
|
||||||
|
type Output = Diagnostics;
|
||||||
|
|
||||||
|
fn add(mut self, other: Self) -> Self::Output {
|
||||||
|
self += other;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl AddAssign for Diagnostics {
|
impl AddAssign for Diagnostics {
|
||||||
fn add_assign(&mut self, other: Self) {
|
fn add_assign(&mut self, other: Self) {
|
||||||
self.messages.extend(other.messages);
|
self.messages.extend(other.messages);
|
||||||
|
|
|
@ -13,7 +13,7 @@ const BIN_NAME: &str = "ruff";
|
||||||
#[test]
|
#[test]
|
||||||
fn default_options() {
|
fn default_options() {
|
||||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
.args(["format", "--isolated"])
|
.args(["format", "--isolated", "--stdin-filename", "test.py"])
|
||||||
.arg("-")
|
.arg("-")
|
||||||
.pass_stdin(r#"
|
.pass_stdin(r#"
|
||||||
def foo(arg1, arg2,):
|
def foo(arg1, arg2,):
|
||||||
|
@ -88,6 +88,108 @@ if condition:
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exclude() -> Result<()> {
|
||||||
|
let tempdir = TempDir::new()?;
|
||||||
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||||
|
fs::write(
|
||||||
|
&ruff_toml,
|
||||||
|
r#"
|
||||||
|
extend-exclude = ["out"]
|
||||||
|
|
||||||
|
[format]
|
||||||
|
exclude = ["test.py", "generated.py"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
tempdir.path().join("main.py"),
|
||||||
|
r#"
|
||||||
|
from test import say_hy
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
say_hy("dear Ruff contributor")
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Excluded file but passed to the CLI directly, should be formatted
|
||||||
|
let test_path = tempdir.path().join("test.py");
|
||||||
|
fs::write(
|
||||||
|
&test_path,
|
||||||
|
r#"
|
||||||
|
def say_hy(name: str):
|
||||||
|
print(f"Hy {name}")"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
tempdir.path().join("generated.py"),
|
||||||
|
r#"NUMBERS = [
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
|
||||||
|
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
|
||||||
|
]
|
||||||
|
OTHER = "OTHER"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let out_dir = tempdir.path().join("out");
|
||||||
|
fs::create_dir(&out_dir)?;
|
||||||
|
|
||||||
|
fs::write(out_dir.join("a.py"), "a = a")?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.current_dir(tempdir.path())
|
||||||
|
.args(["format", "--check", "--config"])
|
||||||
|
.arg(ruff_toml.file_name().unwrap())
|
||||||
|
// Explicitly pass test.py, should be formatted regardless of it being excluded by format.exclude
|
||||||
|
.arg(test_path.file_name().unwrap())
|
||||||
|
// Format all other files in the directory, should respect the `exclude` and `format.exclude` options
|
||||||
|
.arg("."), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
Would reformat: main.py
|
||||||
|
Would reformat: test.py
|
||||||
|
2 files would be reformatted
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||||
|
"###);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exclude_stdin() -> Result<()> {
|
||||||
|
let tempdir = TempDir::new()?;
|
||||||
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||||
|
fs::write(
|
||||||
|
&ruff_toml,
|
||||||
|
r#"
|
||||||
|
extend-select = ["B", "Q"]
|
||||||
|
|
||||||
|
[format]
|
||||||
|
exclude = ["generated.py"]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.current_dir(tempdir.path())
|
||||||
|
.args(["format", "--config", &ruff_toml.file_name().unwrap().to_string_lossy(), "--stdin-filename", "generated.py", "-"])
|
||||||
|
.pass_stdin(r#"
|
||||||
|
from test import say_hy
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
say_hy("dear Ruff contributor")
|
||||||
|
"#), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `ruff format` is not yet stable, and subject to change in future versions.
|
||||||
|
"###);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_option_inheritance() -> Result<()> {
|
fn format_option_inheritance() -> Result<()> {
|
||||||
let tempdir = TempDir::new()?;
|
let tempdir = TempDir::new()?;
|
||||||
|
|
|
@ -31,14 +31,15 @@ inline-quotes = "single"
|
||||||
.args(STDIN_BASE_OPTIONS)
|
.args(STDIN_BASE_OPTIONS)
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
.arg(&ruff_toml)
|
.arg(&ruff_toml)
|
||||||
|
.args(["--stdin-filename", "test.py"])
|
||||||
.arg("-")
|
.arg("-")
|
||||||
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
|
.pass_stdin(r#"a = "abcba".strip("aba")"#), @r###"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
-:1:5: Q000 [*] Double quotes found but single quotes preferred
|
test.py:1:5: Q000 [*] Double quotes found but single quotes preferred
|
||||||
-:1:5: B005 Using `.strip()` with multi-character strings is misleading
|
test.py:1:5: B005 Using `.strip()` with multi-character strings is misleading
|
||||||
-:1:19: Q000 [*] Double quotes found but single quotes preferred
|
test.py:1:19: Q000 [*] Double quotes found but single quotes preferred
|
||||||
Found 3 errors.
|
Found 3 errors.
|
||||||
[*] 2 fixable with the `--fix` option.
|
[*] 2 fixable with the `--fix` option.
|
||||||
|
|
||||||
|
@ -155,3 +156,117 @@ inline-quotes = "single"
|
||||||
"###);
|
"###);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exclude() -> Result<()> {
|
||||||
|
let tempdir = TempDir::new()?;
|
||||||
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||||
|
fs::write(
|
||||||
|
&ruff_toml,
|
||||||
|
r#"
|
||||||
|
extend-select = ["B", "Q"]
|
||||||
|
extend-exclude = ["out"]
|
||||||
|
|
||||||
|
[lint]
|
||||||
|
exclude = ["test.py", "generated.py"]
|
||||||
|
|
||||||
|
[lint.flake8-quotes]
|
||||||
|
inline-quotes = "single"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
tempdir.path().join("main.py"),
|
||||||
|
r#"
|
||||||
|
from test import say_hy
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
say_hy("dear Ruff contributor")
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Excluded file but passed to the CLI directly, should be linted
|
||||||
|
let test_path = tempdir.path().join("test.py");
|
||||||
|
fs::write(
|
||||||
|
&test_path,
|
||||||
|
r#"
|
||||||
|
def say_hy(name: str):
|
||||||
|
print(f"Hy {name}")"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
fs::write(
|
||||||
|
tempdir.path().join("generated.py"),
|
||||||
|
r#"NUMBERS = [
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
|
||||||
|
10, 11, 12, 13, 14, 15, 16, 17, 18, 19
|
||||||
|
]
|
||||||
|
OTHER = "OTHER"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let out_dir = tempdir.path().join("out");
|
||||||
|
fs::create_dir(&out_dir)?;
|
||||||
|
|
||||||
|
fs::write(out_dir.join("a.py"), r#"a = "a""#)?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.current_dir(tempdir.path())
|
||||||
|
.arg("check")
|
||||||
|
.args(STDIN_BASE_OPTIONS)
|
||||||
|
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
|
||||||
|
// Explicitly pass test.py, should be linted regardless of it being excluded by lint.exclude
|
||||||
|
.arg(test_path.file_name().unwrap())
|
||||||
|
// Lint all other files in the directory, should respect the `exclude` and `lint.exclude` options
|
||||||
|
.arg("."), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
main.py:4:16: Q000 [*] Double quotes found but single quotes preferred
|
||||||
|
main.py:5:12: Q000 [*] Double quotes found but single quotes preferred
|
||||||
|
test.py:3:15: Q000 [*] Double quotes found but single quotes preferred
|
||||||
|
Found 3 errors.
|
||||||
|
[*] 3 fixable with the `--fix` option.
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exclude_stdin() -> Result<()> {
|
||||||
|
let tempdir = TempDir::new()?;
|
||||||
|
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||||
|
fs::write(
|
||||||
|
&ruff_toml,
|
||||||
|
r#"
|
||||||
|
extend-select = ["B", "Q"]
|
||||||
|
|
||||||
|
[lint]
|
||||||
|
exclude = ["generated.py"]
|
||||||
|
|
||||||
|
[lint.flake8-quotes]
|
||||||
|
inline-quotes = "single"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||||
|
.current_dir(tempdir.path())
|
||||||
|
.arg("check")
|
||||||
|
.args(STDIN_BASE_OPTIONS)
|
||||||
|
.args(["--config", &ruff_toml.file_name().unwrap().to_string_lossy()])
|
||||||
|
.args(["--stdin-filename", "generated.py"])
|
||||||
|
.arg("-")
|
||||||
|
.pass_stdin(r#"
|
||||||
|
from test import say_hy
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
say_hy("dear Ruff contributor")
|
||||||
|
"#), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ use std::{fmt, fs, io, iter};
|
||||||
|
|
||||||
use anyhow::{bail, format_err, Context, Error};
|
use anyhow::{bail, format_err, Context, Error};
|
||||||
use clap::{CommandFactory, FromArgMatches};
|
use clap::{CommandFactory, FromArgMatches};
|
||||||
use ignore::DirEntry;
|
|
||||||
use imara_diff::intern::InternedInput;
|
use imara_diff::intern::InternedInput;
|
||||||
use imara_diff::sink::Counter;
|
use imara_diff::sink::Counter;
|
||||||
use imara_diff::{diff, Algorithm};
|
use imara_diff::{diff, Algorithm};
|
||||||
|
@ -36,14 +35,14 @@ use ruff_linter::settings::types::{FilePattern, FilePatternSet};
|
||||||
use ruff_python_formatter::{
|
use ruff_python_formatter::{
|
||||||
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
|
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
|
||||||
};
|
};
|
||||||
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, Resolver};
|
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
|
||||||
|
|
||||||
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
|
/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn ruff_check_paths(
|
fn ruff_check_paths(
|
||||||
dirs: &[PathBuf],
|
dirs: &[PathBuf],
|
||||||
) -> anyhow::Result<(
|
) -> anyhow::Result<(
|
||||||
Vec<Result<DirEntry, ignore::Error>>,
|
Vec<Result<ResolvedFile, ignore::Error>>,
|
||||||
Resolver,
|
Resolver,
|
||||||
PyprojectConfig,
|
PyprojectConfig,
|
||||||
)> {
|
)> {
|
||||||
|
@ -467,9 +466,9 @@ fn format_dev_project(
|
||||||
let iter = { paths.into_par_iter() };
|
let iter = { paths.into_par_iter() };
|
||||||
#[cfg(feature = "singlethreaded")]
|
#[cfg(feature = "singlethreaded")]
|
||||||
let iter = { paths.into_iter() };
|
let iter = { paths.into_iter() };
|
||||||
iter.map(|dir_entry| {
|
iter.map(|path| {
|
||||||
let result = format_dir_entry(
|
let result = format_dir_entry(
|
||||||
dir_entry,
|
path,
|
||||||
stability_check,
|
stability_check,
|
||||||
write,
|
write,
|
||||||
&black_options,
|
&black_options,
|
||||||
|
@ -527,24 +526,20 @@ fn format_dev_project(
|
||||||
|
|
||||||
/// Error handling in between walkdir and `format_dev_file`
|
/// Error handling in between walkdir and `format_dev_file`
|
||||||
fn format_dir_entry(
|
fn format_dir_entry(
|
||||||
dir_entry: Result<DirEntry, ignore::Error>,
|
resolved_file: Result<ResolvedFile, ignore::Error>,
|
||||||
stability_check: bool,
|
stability_check: bool,
|
||||||
write: bool,
|
write: bool,
|
||||||
options: &BlackOptions,
|
options: &BlackOptions,
|
||||||
resolver: &Resolver,
|
resolver: &Resolver,
|
||||||
pyproject_config: &PyprojectConfig,
|
pyproject_config: &PyprojectConfig,
|
||||||
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
|
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
|
||||||
let dir_entry = match dir_entry.context("Iterating the files in the repository failed") {
|
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
|
||||||
Ok(dir_entry) => dir_entry,
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
};
|
|
||||||
let file = dir_entry.path().to_path_buf();
|
|
||||||
// For some reason it does not filter in the beginning
|
// For some reason it does not filter in the beginning
|
||||||
if dir_entry.file_name() == "pyproject.toml" {
|
if resolved_file.file_name() == "pyproject.toml" {
|
||||||
return Ok((Ok(Statistics::default()), file));
|
return Ok((Ok(Statistics::default()), resolved_file.into_path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = dir_entry.path().to_path_buf();
|
let path = resolved_file.into_path();
|
||||||
let mut options = options.to_py_format_options(&path);
|
let mut options = options.to_py_format_options(&path);
|
||||||
|
|
||||||
let settings = resolver.resolve(&path, pyproject_config);
|
let settings = resolver.resolve(&path, pyproject_config);
|
||||||
|
|
|
@ -23,7 +23,7 @@ use crate::rules::{
|
||||||
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
|
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
|
||||||
pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade,
|
pycodestyle, pydocstyle, pyflakes, pylint, pyupgrade,
|
||||||
};
|
};
|
||||||
use crate::settings::types::{PerFileIgnore, PythonVersion};
|
use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion};
|
||||||
use crate::{codes, RuleSelector};
|
use crate::{codes, RuleSelector};
|
||||||
|
|
||||||
use super::line_width::{LineLength, TabSize};
|
use super::line_width::{LineLength, TabSize};
|
||||||
|
@ -38,6 +38,7 @@ pub mod types;
|
||||||
|
|
||||||
#[derive(Debug, CacheKey)]
|
#[derive(Debug, CacheKey)]
|
||||||
pub struct LinterSettings {
|
pub struct LinterSettings {
|
||||||
|
pub exclude: FilePatternSet,
|
||||||
pub project_root: PathBuf,
|
pub project_root: PathBuf,
|
||||||
|
|
||||||
pub rules: RuleTable,
|
pub rules: RuleTable,
|
||||||
|
@ -131,6 +132,7 @@ impl LinterSettings {
|
||||||
|
|
||||||
pub fn new(project_root: &Path) -> Self {
|
pub fn new(project_root: &Path) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
exclude: FilePatternSet::default(),
|
||||||
target_version: PythonVersion::default(),
|
target_version: PythonVersion::default(),
|
||||||
project_root: project_root.to_path_buf(),
|
project_root: project_root.to_path_buf(),
|
||||||
rules: DEFAULT_SELECTORS
|
rules: DEFAULT_SELECTORS
|
||||||
|
|
|
@ -150,7 +150,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
|
||||||
N: Ranged,
|
N: Ranged,
|
||||||
Separator: Format<PyFormatContext<'ast>>,
|
Separator: Format<PyFormatContext<'ast>>,
|
||||||
{
|
{
|
||||||
self.result = self.result.and_then(|_| {
|
self.result = self.result.and_then(|()| {
|
||||||
if self.entries.is_one_or_more() {
|
if self.entries.is_one_or_more() {
|
||||||
write!(self.fmt, [token(","), separator])?;
|
write!(self.fmt, [token(","), separator])?;
|
||||||
}
|
}
|
||||||
|
@ -190,7 +190,7 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn finish(&mut self) -> FormatResult<()> {
|
pub(crate) fn finish(&mut self) -> FormatResult<()> {
|
||||||
self.result.and_then(|_| {
|
self.result.and_then(|()| {
|
||||||
if let Some(last_end) = self.entries.position() {
|
if let Some(last_end) = self.entries.position() {
|
||||||
let magic_trailing_comma = has_magic_trailing_comma(
|
let magic_trailing_comma = has_magic_trailing_comma(
|
||||||
TextRange::new(last_end, self.sequence_end),
|
TextRange::new(last_end, self.sequence_end),
|
||||||
|
|
|
@ -22,7 +22,7 @@ use ruff_python_trivia::CommentRanges;
|
||||||
use ruff_source_file::{Locator, SourceLocation};
|
use ruff_source_file::{Locator, SourceLocation};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
use ruff_workspace::configuration::Configuration;
|
use ruff_workspace::configuration::Configuration;
|
||||||
use ruff_workspace::options::{FormatOptions, LintOptions, Options};
|
use ruff_workspace::options::{FormatOptions, LintCommonOptions, LintOptions, Options};
|
||||||
use ruff_workspace::Settings;
|
use ruff_workspace::Settings;
|
||||||
|
|
||||||
#[wasm_bindgen(typescript_custom_section)]
|
#[wasm_bindgen(typescript_custom_section)]
|
||||||
|
@ -130,6 +130,7 @@ impl Workspace {
|
||||||
target_version: Some(PythonVersion::default()),
|
target_version: Some(PythonVersion::default()),
|
||||||
|
|
||||||
lint: Some(LintOptions {
|
lint: Some(LintOptions {
|
||||||
|
common: LintCommonOptions {
|
||||||
allowed_confusables: Some(Vec::default()),
|
allowed_confusables: Some(Vec::default()),
|
||||||
dummy_variable_rgx: Some(DUMMY_VARIABLE_RGX.as_str().to_string()),
|
dummy_variable_rgx: Some(DUMMY_VARIABLE_RGX.as_str().to_string()),
|
||||||
ignore: Some(Vec::default()),
|
ignore: Some(Vec::default()),
|
||||||
|
@ -137,6 +138,8 @@ impl Workspace {
|
||||||
extend_fixable: Some(Vec::default()),
|
extend_fixable: Some(Vec::default()),
|
||||||
extend_select: Some(Vec::default()),
|
extend_select: Some(Vec::default()),
|
||||||
external: Some(Vec::default()),
|
external: Some(Vec::default()),
|
||||||
|
..LintCommonOptions::default()
|
||||||
|
},
|
||||||
|
|
||||||
..LintOptions::default()
|
..LintOptions::default()
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -157,6 +157,7 @@ impl Configuration {
|
||||||
let format_defaults = FormatterSettings::default();
|
let format_defaults = FormatterSettings::default();
|
||||||
// TODO(micha): Support changing the tab-width but disallow changing the number of spaces
|
// TODO(micha): Support changing the tab-width but disallow changing the number of spaces
|
||||||
let formatter = FormatterSettings {
|
let formatter = FormatterSettings {
|
||||||
|
exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?,
|
||||||
preview: match format.preview.unwrap_or(preview) {
|
preview: match format.preview.unwrap_or(preview) {
|
||||||
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
|
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
|
||||||
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
|
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
|
||||||
|
@ -204,6 +205,7 @@ impl Configuration {
|
||||||
|
|
||||||
linter: LinterSettings {
|
linter: LinterSettings {
|
||||||
rules: lint.as_rule_table(preview),
|
rules: lint.as_rule_table(preview),
|
||||||
|
exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?,
|
||||||
target_version,
|
target_version,
|
||||||
project_root: project_root.to_path_buf(),
|
project_root: project_root.to_path_buf(),
|
||||||
allowed_confusables: lint
|
allowed_confusables: lint
|
||||||
|
@ -365,10 +367,14 @@ impl Configuration {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_options(options: Options, project_root: &Path) -> Result<Self> {
|
pub fn from_options(options: Options, project_root: &Path) -> Result<Self> {
|
||||||
let lint = if let Some(lint) = options.lint {
|
let lint = if let Some(mut lint) = options.lint {
|
||||||
lint.combine(options.lint_top_level)
|
lint.common = lint.common.combine(options.lint_top_level);
|
||||||
|
lint
|
||||||
} else {
|
} else {
|
||||||
options.lint_top_level
|
LintOptions {
|
||||||
|
common: options.lint_top_level,
|
||||||
|
..LintOptions::default()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -455,7 +461,10 @@ impl Configuration {
|
||||||
target_version: options.target_version,
|
target_version: options.target_version,
|
||||||
|
|
||||||
lint: LintConfiguration::from_options(lint, project_root)?,
|
lint: LintConfiguration::from_options(lint, project_root)?,
|
||||||
format: FormatConfiguration::from_options(options.format.unwrap_or_default())?,
|
format: FormatConfiguration::from_options(
|
||||||
|
options.format.unwrap_or_default(),
|
||||||
|
project_root,
|
||||||
|
)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -501,6 +510,8 @@ impl Configuration {
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct LintConfiguration {
|
pub struct LintConfiguration {
|
||||||
|
pub exclude: Option<Vec<FilePattern>>,
|
||||||
|
|
||||||
// Rule selection
|
// Rule selection
|
||||||
pub extend_per_file_ignores: Vec<PerFileIgnore>,
|
pub extend_per_file_ignores: Vec<PerFileIgnore>,
|
||||||
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
|
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
|
||||||
|
@ -550,33 +561,47 @@ pub struct LintConfiguration {
|
||||||
impl LintConfiguration {
|
impl LintConfiguration {
|
||||||
fn from_options(options: LintOptions, project_root: &Path) -> Result<Self> {
|
fn from_options(options: LintOptions, project_root: &Path) -> Result<Self> {
|
||||||
Ok(LintConfiguration {
|
Ok(LintConfiguration {
|
||||||
|
exclude: options.exclude.map(|paths| {
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|pattern| {
|
||||||
|
let absolute = fs::normalize_path_to(&pattern, project_root);
|
||||||
|
FilePattern::User(pattern, absolute)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
|
|
||||||
rule_selections: vec![RuleSelection {
|
rule_selections: vec![RuleSelection {
|
||||||
select: options.select,
|
select: options.common.select,
|
||||||
ignore: options
|
ignore: options
|
||||||
|
.common
|
||||||
.ignore
|
.ignore
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.chain(options.extend_ignore.into_iter().flatten())
|
.chain(options.common.extend_ignore.into_iter().flatten())
|
||||||
.collect(),
|
.collect(),
|
||||||
extend_select: options.extend_select.unwrap_or_default(),
|
extend_select: options.common.extend_select.unwrap_or_default(),
|
||||||
fixable: options.fixable,
|
fixable: options.common.fixable,
|
||||||
unfixable: options
|
unfixable: options
|
||||||
|
.common
|
||||||
.unfixable
|
.unfixable
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.chain(options.extend_unfixable.into_iter().flatten())
|
.chain(options.common.extend_unfixable.into_iter().flatten())
|
||||||
.collect(),
|
.collect(),
|
||||||
extend_fixable: options.extend_fixable.unwrap_or_default(),
|
extend_fixable: options.common.extend_fixable.unwrap_or_default(),
|
||||||
}],
|
}],
|
||||||
extend_safe_fixes: options.extend_safe_fixes.unwrap_or_default(),
|
extend_safe_fixes: options.common.extend_safe_fixes.unwrap_or_default(),
|
||||||
extend_unsafe_fixes: options.extend_unsafe_fixes.unwrap_or_default(),
|
extend_unsafe_fixes: options.common.extend_unsafe_fixes.unwrap_or_default(),
|
||||||
allowed_confusables: options.allowed_confusables,
|
allowed_confusables: options.common.allowed_confusables,
|
||||||
dummy_variable_rgx: options
|
dummy_variable_rgx: options
|
||||||
|
.common
|
||||||
.dummy_variable_rgx
|
.dummy_variable_rgx
|
||||||
.map(|pattern| Regex::new(&pattern))
|
.map(|pattern| Regex::new(&pattern))
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
|
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
|
||||||
extend_per_file_ignores: options
|
extend_per_file_ignores: options
|
||||||
|
.common
|
||||||
.extend_per_file_ignores
|
.extend_per_file_ignores
|
||||||
.map(|per_file_ignores| {
|
.map(|per_file_ignores| {
|
||||||
per_file_ignores
|
per_file_ignores
|
||||||
|
@ -587,10 +612,10 @@ impl LintConfiguration {
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
external: options.external,
|
external: options.common.external,
|
||||||
ignore_init_module_imports: options.ignore_init_module_imports,
|
ignore_init_module_imports: options.common.ignore_init_module_imports,
|
||||||
explicit_preview_rules: options.explicit_preview_rules,
|
explicit_preview_rules: options.common.explicit_preview_rules,
|
||||||
per_file_ignores: options.per_file_ignores.map(|per_file_ignores| {
|
per_file_ignores: options.common.per_file_ignores.map(|per_file_ignores| {
|
||||||
per_file_ignores
|
per_file_ignores
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(pattern, prefixes)| {
|
.map(|(pattern, prefixes)| {
|
||||||
|
@ -598,34 +623,34 @@ impl LintConfiguration {
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}),
|
}),
|
||||||
task_tags: options.task_tags,
|
task_tags: options.common.task_tags,
|
||||||
logger_objects: options.logger_objects,
|
logger_objects: options.common.logger_objects,
|
||||||
typing_modules: options.typing_modules,
|
typing_modules: options.common.typing_modules,
|
||||||
// Plugins
|
// Plugins
|
||||||
flake8_annotations: options.flake8_annotations,
|
flake8_annotations: options.common.flake8_annotations,
|
||||||
flake8_bandit: options.flake8_bandit,
|
flake8_bandit: options.common.flake8_bandit,
|
||||||
flake8_bugbear: options.flake8_bugbear,
|
flake8_bugbear: options.common.flake8_bugbear,
|
||||||
flake8_builtins: options.flake8_builtins,
|
flake8_builtins: options.common.flake8_builtins,
|
||||||
flake8_comprehensions: options.flake8_comprehensions,
|
flake8_comprehensions: options.common.flake8_comprehensions,
|
||||||
flake8_copyright: options.flake8_copyright,
|
flake8_copyright: options.common.flake8_copyright,
|
||||||
flake8_errmsg: options.flake8_errmsg,
|
flake8_errmsg: options.common.flake8_errmsg,
|
||||||
flake8_gettext: options.flake8_gettext,
|
flake8_gettext: options.common.flake8_gettext,
|
||||||
flake8_implicit_str_concat: options.flake8_implicit_str_concat,
|
flake8_implicit_str_concat: options.common.flake8_implicit_str_concat,
|
||||||
flake8_import_conventions: options.flake8_import_conventions,
|
flake8_import_conventions: options.common.flake8_import_conventions,
|
||||||
flake8_pytest_style: options.flake8_pytest_style,
|
flake8_pytest_style: options.common.flake8_pytest_style,
|
||||||
flake8_quotes: options.flake8_quotes,
|
flake8_quotes: options.common.flake8_quotes,
|
||||||
flake8_self: options.flake8_self,
|
flake8_self: options.common.flake8_self,
|
||||||
flake8_tidy_imports: options.flake8_tidy_imports,
|
flake8_tidy_imports: options.common.flake8_tidy_imports,
|
||||||
flake8_type_checking: options.flake8_type_checking,
|
flake8_type_checking: options.common.flake8_type_checking,
|
||||||
flake8_unused_arguments: options.flake8_unused_arguments,
|
flake8_unused_arguments: options.common.flake8_unused_arguments,
|
||||||
isort: options.isort,
|
isort: options.common.isort,
|
||||||
mccabe: options.mccabe,
|
mccabe: options.common.mccabe,
|
||||||
pep8_naming: options.pep8_naming,
|
pep8_naming: options.common.pep8_naming,
|
||||||
pycodestyle: options.pycodestyle,
|
pycodestyle: options.common.pycodestyle,
|
||||||
pydocstyle: options.pydocstyle,
|
pydocstyle: options.common.pydocstyle,
|
||||||
pyflakes: options.pyflakes,
|
pyflakes: options.common.pyflakes,
|
||||||
pylint: options.pylint,
|
pylint: options.common.pylint,
|
||||||
pyupgrade: options.pyupgrade,
|
pyupgrade: options.common.pyupgrade,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -861,6 +886,7 @@ impl LintConfiguration {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn combine(self, config: Self) -> Self {
|
pub fn combine(self, config: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
exclude: self.exclude.or(config.exclude),
|
||||||
rule_selections: config
|
rule_selections: config
|
||||||
.rule_selections
|
.rule_selections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -935,21 +961,28 @@ impl LintConfiguration {
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct FormatConfiguration {
|
pub struct FormatConfiguration {
|
||||||
|
pub exclude: Option<Vec<FilePattern>>,
|
||||||
pub preview: Option<PreviewMode>,
|
pub preview: Option<PreviewMode>,
|
||||||
|
|
||||||
pub indent_style: Option<IndentStyle>,
|
pub indent_style: Option<IndentStyle>,
|
||||||
|
|
||||||
pub quote_style: Option<QuoteStyle>,
|
pub quote_style: Option<QuoteStyle>,
|
||||||
|
|
||||||
pub magic_trailing_comma: Option<MagicTrailingComma>,
|
pub magic_trailing_comma: Option<MagicTrailingComma>,
|
||||||
|
|
||||||
pub line_ending: Option<LineEnding>,
|
pub line_ending: Option<LineEnding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FormatConfiguration {
|
impl FormatConfiguration {
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub fn from_options(options: FormatOptions) -> Result<Self> {
|
pub fn from_options(options: FormatOptions, project_root: &Path) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
exclude: options.exclude.map(|paths| {
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|pattern| {
|
||||||
|
let absolute = fs::normalize_path_to(&pattern, project_root);
|
||||||
|
FilePattern::User(pattern, absolute)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}),
|
||||||
preview: options.preview.map(PreviewMode::from),
|
preview: options.preview.map(PreviewMode::from),
|
||||||
indent_style: options.indent_style,
|
indent_style: options.indent_style,
|
||||||
quote_style: options.quote_style,
|
quote_style: options.quote_style,
|
||||||
|
@ -968,6 +1001,7 @@ impl FormatConfiguration {
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
pub fn combine(self, other: Self) -> Self {
|
pub fn combine(self, other: Self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
exclude: self.exclude.or(other.exclude),
|
||||||
preview: self.preview.or(other.preview),
|
preview: self.preview.or(other.preview),
|
||||||
indent_style: self.indent_style.or(other.indent_style),
|
indent_style: self.indent_style.or(other.indent_style),
|
||||||
quote_style: self.quote_style.or(other.quote_style),
|
quote_style: self.quote_style.or(other.quote_style),
|
||||||
|
|
|
@ -153,7 +153,7 @@ pub struct Options {
|
||||||
pub preview: Option<bool>,
|
pub preview: Option<bool>,
|
||||||
|
|
||||||
// File resolver options
|
// File resolver options
|
||||||
/// A list of file patterns to exclude from linting.
|
/// A list of file patterns to exclude from formatting and linting.
|
||||||
///
|
///
|
||||||
/// Exclusions are based on globs, and can be either:
|
/// Exclusions are based on globs, and can be either:
|
||||||
///
|
///
|
||||||
|
@ -178,7 +178,7 @@ pub struct Options {
|
||||||
)]
|
)]
|
||||||
pub exclude: Option<Vec<String>>,
|
pub exclude: Option<Vec<String>>,
|
||||||
|
|
||||||
/// A list of file patterns to omit from linting, in addition to those
|
/// A list of file patterns to omit from formatting and linting, in addition to those
|
||||||
/// specified by `exclude`.
|
/// specified by `exclude`.
|
||||||
///
|
///
|
||||||
/// Exclusions are based on globs, and can be either:
|
/// Exclusions are based on globs, and can be either:
|
||||||
|
@ -377,13 +377,46 @@ pub struct Options {
|
||||||
|
|
||||||
/// The lint sections specified at the top level.
|
/// The lint sections specified at the top level.
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub lint_top_level: LintOptions,
|
pub lint_top_level: LintCommonOptions,
|
||||||
|
|
||||||
/// Options to configure code formatting.
|
/// Options to configure code formatting.
|
||||||
#[option_group]
|
#[option_group]
|
||||||
pub format: Option<FormatOptions>,
|
pub format: Option<FormatOptions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
|
#[derive(Debug, PartialEq, Eq, Default, OptionsMetadata, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
|
pub struct LintOptions {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub common: LintCommonOptions,
|
||||||
|
|
||||||
|
/// A list of file patterns to exclude from linting in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).
|
||||||
|
///
|
||||||
|
/// Exclusions are based on globs, and can be either:
|
||||||
|
///
|
||||||
|
/// - Single-path patterns, like `.mypy_cache` (to exclude any directory
|
||||||
|
/// named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
|
||||||
|
/// `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
|
||||||
|
/// - Relative patterns, like `directory/foo.py` (to exclude that specific
|
||||||
|
/// file) or `directory/*.py` (to exclude any Python files in
|
||||||
|
/// `directory`). Note that these paths are relative to the project root
|
||||||
|
/// (e.g., the directory containing your `pyproject.toml`).
|
||||||
|
///
|
||||||
|
/// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
|
||||||
|
#[option(
|
||||||
|
default = r#"[]"#,
|
||||||
|
value_type = "list[str]",
|
||||||
|
example = r#"
|
||||||
|
exclude = ["generated"]
|
||||||
|
"#
|
||||||
|
)]
|
||||||
|
pub exclude: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: This struct should be inlined into [`LintOptions`] once support for the top-level lint settings
|
||||||
|
// is removed.
|
||||||
|
|
||||||
/// Experimental section to configure Ruff's linting. This new section will eventually
|
/// Experimental section to configure Ruff's linting. This new section will eventually
|
||||||
/// replace the top-level linting options.
|
/// replace the top-level linting options.
|
||||||
///
|
///
|
||||||
|
@ -393,7 +426,7 @@ pub struct Options {
|
||||||
Debug, PartialEq, Eq, Default, OptionsMetadata, CombineOptions, Serialize, Deserialize,
|
Debug, PartialEq, Eq, Default, OptionsMetadata, CombineOptions, Serialize, Deserialize,
|
||||||
)]
|
)]
|
||||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
pub struct LintOptions {
|
pub struct LintCommonOptions {
|
||||||
/// A list of allowed "confusable" Unicode characters to ignore when
|
/// A list of allowed "confusable" Unicode characters to ignore when
|
||||||
/// enforcing `RUF001`, `RUF002`, and `RUF003`.
|
/// enforcing `RUF001`, `RUF002`, and `RUF003`.
|
||||||
#[option(
|
#[option(
|
||||||
|
@ -2469,6 +2502,31 @@ impl PyUpgradeOptions {
|
||||||
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct FormatOptions {
|
pub struct FormatOptions {
|
||||||
|
/// A list of file patterns to exclude from formatting in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).
|
||||||
|
///
|
||||||
|
/// Exclusions are based on globs, and can be either:
|
||||||
|
///
|
||||||
|
/// - Single-path patterns, like `.mypy_cache` (to exclude any directory
|
||||||
|
/// named `.mypy_cache` in the tree), `foo.py` (to exclude any file named
|
||||||
|
/// `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ).
|
||||||
|
/// - Relative patterns, like `directory/foo.py` (to exclude that specific
|
||||||
|
/// file) or `directory/*.py` (to exclude any Python files in
|
||||||
|
/// `directory`). Note that these paths are relative to the project root
|
||||||
|
/// (e.g., the directory containing your `pyproject.toml`).
|
||||||
|
///
|
||||||
|
/// For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
|
||||||
|
///
|
||||||
|
/// Note that you'll typically want to use
|
||||||
|
/// [`extend-exclude`](#extend-exclude) to modify the excluded paths.
|
||||||
|
#[option(
|
||||||
|
default = r#"[]"#,
|
||||||
|
value_type = "list[str]",
|
||||||
|
example = r#"
|
||||||
|
exclude = ["generated"]
|
||||||
|
"#
|
||||||
|
)]
|
||||||
|
pub exclude: Option<Vec<String>>,
|
||||||
|
|
||||||
/// Whether to enable the unstable preview style formatting.
|
/// Whether to enable the unstable preview style formatting.
|
||||||
#[option(
|
#[option(
|
||||||
default = "false",
|
default = "false",
|
||||||
|
|
|
@ -161,7 +161,7 @@ mod tests {
|
||||||
use ruff_linter::line_width::LineLength;
|
use ruff_linter::line_width::LineLength;
|
||||||
use ruff_linter::settings::types::PatternPrefixPair;
|
use ruff_linter::settings::types::PatternPrefixPair;
|
||||||
|
|
||||||
use crate::options::{LintOptions, Options};
|
use crate::options::{LintCommonOptions, Options};
|
||||||
use crate::pyproject::{find_settings_toml, parse_pyproject_toml, Pyproject, Tools};
|
use crate::pyproject::{find_settings_toml, parse_pyproject_toml, Pyproject, Tools};
|
||||||
use crate::tests::test_resource_path;
|
use crate::tests::test_resource_path;
|
||||||
|
|
||||||
|
@ -236,9 +236,9 @@ select = ["E501"]
|
||||||
pyproject.tool,
|
pyproject.tool,
|
||||||
Some(Tools {
|
Some(Tools {
|
||||||
ruff: Some(Options {
|
ruff: Some(Options {
|
||||||
lint_top_level: LintOptions {
|
lint_top_level: LintCommonOptions {
|
||||||
select: Some(vec![codes::Pycodestyle::E501.into()]),
|
select: Some(vec![codes::Pycodestyle::E501.into()]),
|
||||||
..LintOptions::default()
|
..LintCommonOptions::default()
|
||||||
},
|
},
|
||||||
..Options::default()
|
..Options::default()
|
||||||
})
|
})
|
||||||
|
@ -257,10 +257,10 @@ ignore = ["E501"]
|
||||||
pyproject.tool,
|
pyproject.tool,
|
||||||
Some(Tools {
|
Some(Tools {
|
||||||
ruff: Some(Options {
|
ruff: Some(Options {
|
||||||
lint_top_level: LintOptions {
|
lint_top_level: LintCommonOptions {
|
||||||
extend_select: Some(vec![codes::Ruff::_100.into()]),
|
extend_select: Some(vec![codes::Ruff::_100.into()]),
|
||||||
ignore: Some(vec![codes::Pycodestyle::E501.into()]),
|
ignore: Some(vec![codes::Pycodestyle::E501.into()]),
|
||||||
..LintOptions::default()
|
..LintCommonOptions::default()
|
||||||
},
|
},
|
||||||
..Options::default()
|
..Options::default()
|
||||||
})
|
})
|
||||||
|
@ -315,12 +315,12 @@ other-attribute = 1
|
||||||
"with_excluded_file/other_excluded_file.py".to_string(),
|
"with_excluded_file/other_excluded_file.py".to_string(),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
lint_top_level: LintOptions {
|
lint_top_level: LintCommonOptions {
|
||||||
per_file_ignores: Some(FxHashMap::from_iter([(
|
per_file_ignores: Some(FxHashMap::from_iter([(
|
||||||
"__init__.py".to_string(),
|
"__init__.py".to_string(),
|
||||||
vec![codes::Pyflakes::_401.into()]
|
vec![codes::Pyflakes::_401.into()]
|
||||||
)])),
|
)])),
|
||||||
..LintOptions::default()
|
..LintCommonOptions::default()
|
||||||
},
|
},
|
||||||
..Options::default()
|
..Options::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
//! Discover Python files, and their corresponding [`Settings`], from the
|
//! Discover Python files, and their corresponding [`Settings`], from the
|
||||||
//! filesystem.
|
//! filesystem.
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use anyhow::{anyhow, bail};
|
use anyhow::{anyhow, bail};
|
||||||
use ignore::{DirEntry, WalkBuilder, WalkState};
|
use ignore::{WalkBuilder, WalkState};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use path_absolutize::path_dedot;
|
use path_absolutize::path_dedot;
|
||||||
|
@ -276,7 +278,7 @@ pub fn python_files_in_path(
|
||||||
paths: &[PathBuf],
|
paths: &[PathBuf],
|
||||||
pyproject_config: &PyprojectConfig,
|
pyproject_config: &PyprojectConfig,
|
||||||
transformer: &dyn ConfigurationTransformer,
|
transformer: &dyn ConfigurationTransformer,
|
||||||
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
|
) -> Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver)> {
|
||||||
// Normalize every path (e.g., convert from relative to absolute).
|
// Normalize every path (e.g., convert from relative to absolute).
|
||||||
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).unique().collect();
|
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).unique().collect();
|
||||||
|
|
||||||
|
@ -305,13 +307,12 @@ pub fn python_files_in_path(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (first_path, rest_paths) = paths
|
||||||
|
.split_first()
|
||||||
|
.ok_or_else(|| anyhow!("Expected at least one path to search for Python files"))?;
|
||||||
// Create the `WalkBuilder`.
|
// Create the `WalkBuilder`.
|
||||||
let mut builder = WalkBuilder::new(
|
let mut builder = WalkBuilder::new(first_path);
|
||||||
paths
|
for path in rest_paths {
|
||||||
.get(0)
|
|
||||||
.ok_or_else(|| anyhow!("Expected at least one path to search for Python files"))?,
|
|
||||||
);
|
|
||||||
for path in &paths[1..] {
|
|
||||||
builder.add(path);
|
builder.add(path);
|
||||||
}
|
}
|
||||||
builder.standard_filters(pyproject_config.settings.file_resolver.respect_gitignore);
|
builder.standard_filters(pyproject_config.settings.file_resolver.respect_gitignore);
|
||||||
|
@ -321,7 +322,7 @@ pub fn python_files_in_path(
|
||||||
// Run the `WalkParallel` to collect all Python files.
|
// Run the `WalkParallel` to collect all Python files.
|
||||||
let error: std::sync::Mutex<Result<()>> = std::sync::Mutex::new(Ok(()));
|
let error: std::sync::Mutex<Result<()>> = std::sync::Mutex::new(Ok(()));
|
||||||
let resolver: RwLock<Resolver> = RwLock::new(resolver);
|
let resolver: RwLock<Resolver> = RwLock::new(resolver);
|
||||||
let files: std::sync::Mutex<Vec<Result<DirEntry, ignore::Error>>> =
|
let files: std::sync::Mutex<Vec<Result<ResolvedFile, ignore::Error>>> =
|
||||||
std::sync::Mutex::new(vec![]);
|
std::sync::Mutex::new(vec![]);
|
||||||
walker.run(|| {
|
walker.run(|| {
|
||||||
Box::new(|result| {
|
Box::new(|result| {
|
||||||
|
@ -332,18 +333,14 @@ pub fn python_files_in_path(
|
||||||
let resolver = resolver.read().unwrap();
|
let resolver = resolver.read().unwrap();
|
||||||
let settings = resolver.resolve(path, pyproject_config);
|
let settings = resolver.resolve(path, pyproject_config);
|
||||||
if let Some(file_name) = path.file_name() {
|
if let Some(file_name) = path.file_name() {
|
||||||
if !settings.file_resolver.exclude.is_empty()
|
if match_exclusion(path, file_name, &settings.file_resolver.exclude) {
|
||||||
&& match_exclusion(path, file_name, &settings.file_resolver.exclude)
|
|
||||||
{
|
|
||||||
debug!("Ignored path via `exclude`: {:?}", path);
|
debug!("Ignored path via `exclude`: {:?}", path);
|
||||||
return WalkState::Skip;
|
return WalkState::Skip;
|
||||||
} else if !settings.file_resolver.extend_exclude.is_empty()
|
} else if match_exclusion(
|
||||||
&& match_exclusion(
|
|
||||||
path,
|
path,
|
||||||
file_name,
|
file_name,
|
||||||
&settings.file_resolver.extend_exclude,
|
&settings.file_resolver.extend_exclude,
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||||
return WalkState::Skip;
|
return WalkState::Skip;
|
||||||
}
|
}
|
||||||
|
@ -386,13 +383,14 @@ pub fn python_files_in_path(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.as_ref().map_or(true, |entry| {
|
match result {
|
||||||
|
Ok(entry) => {
|
||||||
// Ignore directories
|
// Ignore directories
|
||||||
if entry.file_type().map_or(true, |ft| ft.is_dir()) {
|
let resolved = if entry.file_type().map_or(true, |ft| ft.is_dir()) {
|
||||||
false
|
None
|
||||||
} else if entry.depth() == 0 {
|
} else if entry.depth() == 0 {
|
||||||
// Accept all files that are passed-in directly.
|
// Accept all files that are passed-in directly.
|
||||||
true
|
Some(ResolvedFile::Root(entry.into_path()))
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, check if the file is included.
|
// Otherwise, check if the file is included.
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
@ -400,16 +398,22 @@ pub fn python_files_in_path(
|
||||||
let settings = resolver.resolve(path, pyproject_config);
|
let settings = resolver.resolve(path, pyproject_config);
|
||||||
if settings.file_resolver.include.is_match(path) {
|
if settings.file_resolver.include.is_match(path) {
|
||||||
debug!("Included path via `include`: {:?}", path);
|
debug!("Included path via `include`: {:?}", path);
|
||||||
true
|
Some(ResolvedFile::Nested(entry.into_path()))
|
||||||
} else if settings.file_resolver.extend_include.is_match(path) {
|
} else if settings.file_resolver.extend_include.is_match(path) {
|
||||||
debug!("Included path via `extend-include`: {:?}", path);
|
debug!("Included path via `extend-include`: {:?}", path);
|
||||||
true
|
Some(ResolvedFile::Nested(entry.into_path()))
|
||||||
} else {
|
} else {
|
||||||
false
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(resolved) = resolved {
|
||||||
|
files.lock().unwrap().push(Ok(resolved));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
Err(err) => {
|
||||||
files.lock().unwrap().push(result);
|
files.lock().unwrap().push(Err(err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WalkState::Continue
|
WalkState::Continue
|
||||||
|
@ -421,6 +425,51 @@ pub fn python_files_in_path(
|
||||||
Ok((files.into_inner().unwrap(), resolver.into_inner().unwrap()))
|
Ok((files.into_inner().unwrap(), resolver.into_inner().unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ResolvedFile {
|
||||||
|
/// File explicitly passed to the CLI
|
||||||
|
Root(PathBuf),
|
||||||
|
/// File in a sub-directory
|
||||||
|
Nested(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedFile {
|
||||||
|
pub fn into_path(self) -> PathBuf {
|
||||||
|
match self {
|
||||||
|
ResolvedFile::Root(path) => path,
|
||||||
|
ResolvedFile::Nested(path) => path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
ResolvedFile::Root(root) => root.as_path(),
|
||||||
|
ResolvedFile::Nested(root) => root.as_path(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file_name(&self) -> &OsStr {
|
||||||
|
let path = self.path();
|
||||||
|
path.file_name().unwrap_or(path.as_os_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_root(&self) -> bool {
|
||||||
|
matches!(self, ResolvedFile::Root(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for ResolvedFile {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for ResolvedFile {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.path().cmp(other.path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` if the Python file at [`Path`] is _not_ excluded.
|
/// Return `true` if the Python file at [`Path`] is _not_ excluded.
|
||||||
pub fn python_file_at_path(
|
pub fn python_file_at_path(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
@ -458,25 +507,17 @@ fn is_file_excluded(
|
||||||
) -> bool {
|
) -> bool {
|
||||||
// TODO(charlie): Respect gitignore.
|
// TODO(charlie): Respect gitignore.
|
||||||
for path in path.ancestors() {
|
for path in path.ancestors() {
|
||||||
if path.file_name().is_none() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let settings = resolver.resolve(path, pyproject_strategy);
|
let settings = resolver.resolve(path, pyproject_strategy);
|
||||||
if let Some(file_name) = path.file_name() {
|
if let Some(file_name) = path.file_name() {
|
||||||
if !settings.file_resolver.exclude.is_empty()
|
if match_exclusion(path, file_name, &settings.file_resolver.exclude) {
|
||||||
&& match_exclusion(path, file_name, &settings.file_resolver.exclude)
|
|
||||||
{
|
|
||||||
debug!("Ignored path via `exclude`: {:?}", path);
|
debug!("Ignored path via `exclude`: {:?}", path);
|
||||||
return true;
|
return true;
|
||||||
} else if !settings.file_resolver.extend_exclude.is_empty()
|
} else if match_exclusion(path, file_name, &settings.file_resolver.extend_exclude) {
|
||||||
&& match_exclusion(path, file_name, &settings.file_resolver.extend_exclude)
|
|
||||||
{
|
|
||||||
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
debug!("Ignored path via `extend-exclude`: {:?}", path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug!("Ignored path due to error in parsing: {:?}", path);
|
break;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
if path == settings.file_resolver.project_root {
|
if path == settings.file_resolver.project_root {
|
||||||
// Bail out; we'd end up past the project root on the next iteration
|
// Bail out; we'd end up past the project root on the next iteration
|
||||||
|
@ -489,7 +530,7 @@ fn is_file_excluded(
|
||||||
|
|
||||||
/// Return `true` if the given file should be ignored based on the exclusion
|
/// Return `true` if the given file should be ignored based on the exclusion
|
||||||
/// criteria.
|
/// criteria.
|
||||||
fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
|
pub fn match_exclusion<P: AsRef<Path>, R: AsRef<Path>>(
|
||||||
file_path: P,
|
file_path: P,
|
||||||
file_basename: R,
|
file_basename: R,
|
||||||
exclusion: &globset::GlobSet,
|
exclusion: &globset::GlobSet,
|
||||||
|
@ -515,7 +556,7 @@ mod tests {
|
||||||
use crate::resolver::{
|
use crate::resolver::{
|
||||||
is_file_excluded, match_exclusion, python_files_in_path, resolve_root_settings,
|
is_file_excluded, match_exclusion, python_files_in_path, resolve_root_settings,
|
||||||
ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity,
|
ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity,
|
||||||
Resolver,
|
ResolvedFile, Resolver,
|
||||||
};
|
};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::tests::test_resource_path;
|
use crate::tests::test_resource_path;
|
||||||
|
@ -584,12 +625,12 @@ mod tests {
|
||||||
&NoOpTransformer,
|
&NoOpTransformer,
|
||||||
)?;
|
)?;
|
||||||
let paths = paths
|
let paths = paths
|
||||||
.iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(ignore::DirEntry::path)
|
.map(ResolvedFile::into_path)
|
||||||
.sorted()
|
.sorted()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(paths, &[file2, file1]);
|
assert_eq!(paths, [file2, file1]);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,7 @@ impl FileResolverSettings {
|
||||||
|
|
||||||
#[derive(CacheKey, Clone, Debug)]
|
#[derive(CacheKey, Clone, Debug)]
|
||||||
pub struct FormatterSettings {
|
pub struct FormatterSettings {
|
||||||
|
pub exclude: FilePatternSet,
|
||||||
pub preview: PreviewMode,
|
pub preview: PreviewMode,
|
||||||
|
|
||||||
pub line_width: LineWidth,
|
pub line_width: LineWidth,
|
||||||
|
@ -162,6 +163,7 @@ impl Default for FormatterSettings {
|
||||||
let default_options = PyFormatOptions::default();
|
let default_options = PyFormatOptions::default();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
exclude: FilePatternSet::default(),
|
||||||
preview: ruff_python_formatter::PreviewMode::Disabled,
|
preview: ruff_python_formatter::PreviewMode::Disabled,
|
||||||
line_width: default_options.line_width(),
|
line_width: default_options.line_width(),
|
||||||
line_ending: LineEnding::Lf,
|
line_ending: LineEnding::Lf,
|
||||||
|
|
24
ruff.schema.json
generated
24
ruff.schema.json
generated
|
@ -41,7 +41,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"exclude": {
|
"exclude": {
|
||||||
"description": "A list of file patterns to exclude from linting.\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).\n\nNote that you'll typically want to use [`extend-exclude`](#extend-exclude) to modify the excluded paths.",
|
"description": "A list of file patterns to exclude from formatting and linting.\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).\n\nNote that you'll typically want to use [`extend-exclude`](#extend-exclude) to modify the excluded paths.",
|
||||||
"type": [
|
"type": [
|
||||||
"array",
|
"array",
|
||||||
"null"
|
"null"
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"extend-exclude": {
|
"extend-exclude": {
|
||||||
"description": "A list of file patterns to omit from linting, in addition to those specified by `exclude`.\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).",
|
"description": "A list of file patterns to omit from formatting and linting, in addition to those specified by `exclude`.\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).",
|
||||||
"type": [
|
"type": [
|
||||||
"array",
|
"array",
|
||||||
"null"
|
"null"
|
||||||
|
@ -1209,6 +1209,16 @@
|
||||||
"description": "Experimental: Configures how `ruff format` formats your code.\n\nPlease provide feedback in [this discussion](https://github.com/astral-sh/ruff/discussions/7310).",
|
"description": "Experimental: Configures how `ruff format` formats your code.\n\nPlease provide feedback in [this discussion](https://github.com/astral-sh/ruff/discussions/7310).",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"exclude": {
|
||||||
|
"description": "A list of file patterns to exclude from formatting in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).\n\nNote that you'll typically want to use [`extend-exclude`](#extend-exclude) to modify the excluded paths.",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"indent-style": {
|
"indent-style": {
|
||||||
"description": "Whether to use 4 spaces or hard tabs for indenting code.\n\nDefaults to 4 spaces. We care about accessibility; if you do not need tabs for accessibility, we do not recommend you use them.",
|
"description": "Whether to use 4 spaces or hard tabs for indenting code.\n\nDefaults to 4 spaces. We care about accessibility; if you do not need tabs for accessibility, we do not recommend you use them.",
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
|
@ -1592,6 +1602,16 @@
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"exclude": {
|
||||||
|
"description": "A list of file patterns to exclude from linting in addition to the files excluded globally (see [`exclude`](#exclude), and [`extend-exclude`](#extend-exclude)).\n\nExclusions are based on globs, and can be either:\n\n- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the project root (e.g., the directory containing your `pyproject.toml`).\n\nFor more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
"explicit-preview-rules": {
|
"explicit-preview-rules": {
|
||||||
"description": "Whether to require exact codes to select preview rules. When enabled, preview rules will not be selected by prefixes — the full code of each preview rule will be required to enable the rule.",
|
"description": "Whether to require exact codes to select preview rules. When enabled, preview rules will not be selected by prefixes — the full code of each preview rule will be required to enable the rule.",
|
||||||
"type": [
|
"type": [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue