Compare commits

...

6 Commits

Author SHA1 Message Date
Charlie Marsh
1ea2e93f8e Bump version to 0.0.182 2022-12-14 22:57:22 -05:00
Charlie Marsh
dc180dc277 Negate ignore_names condition 2022-12-14 22:50:26 -05:00
Charlie Marsh
6be910ae07 Use more precise ranges for function and class checks (#1247) 2022-12-14 22:40:00 -05:00
Charlie Marsh
ba85eb846c Run cargo fmt 2022-12-14 21:52:44 -05:00
Charlie Marsh
d067efe265 Treat extend-* configuration options as "always extended" (#1245) 2022-12-14 20:22:40 -05:00
Charlie Marsh
549ea2f85f Ignore any pyproject.toml without a [tool.ruff] section (#1243) 2022-12-14 19:35:52 -05:00
30 changed files with 590 additions and 315 deletions

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.181
rev: v0.0.182
hooks:
- id: ruff

8
Cargo.lock generated
View File

@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.181-dev.0"
version = "0.0.182-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1845,7 +1845,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.181"
version = "0.0.182"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1900,7 +1900,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.181"
version = "0.0.182"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1918,7 +1918,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.181"
version = "0.0.182"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.181"
version = "0.0.182"
edition = "2021"
rust-version = "1.65.0"
@@ -43,7 +43,7 @@ quick-junit = { version = "0.3.2" }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.181", path = "ruff_macros" }
ruff_macros = { version = "0.0.182", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "8d879a53197f9c73062f6160410bdba796a71cbf" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "8d879a53197f9c73062f6160410bdba796a71cbf" }

View File

@@ -155,7 +155,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.181
rev: v0.0.182
hooks:
- id: ruff
```
@@ -343,15 +343,18 @@ directory hierarchy is used for every individual file, with all paths in the `py
There are a few exceptions to these rules:
1. If a configuration file is passed directly via `--config`, those settings are used for across
1. In locating the "closest" `pyproject.toml` file for a given path, Ruff ignore any
`pyproject.toml` files that lack a `[tool.ruff]` section.
2. If a configuration file is passed directly via `--config`, those settings are used for across
files. Any relative paths in that configuration file (like `exclude` globs or `src` paths) are
resolved relative to the _current working directory_.
2. If no `pyproject.toml` file is found in the filesystem hierarchy, Ruff will fall back to using
a default configuration. If a user-specific configuration file exists at `${config_dir}/ruff/pyproject.toml`,
that file will be used instead of the default configuration, with `${config_dir}` being determined
via the [`dirs](https://docs.rs/dirs/4.0.0/dirs/fn.config_dir.html) crate, and all relative paths
being again resolved relative to the _current working directory_.
3. Any `pyproject.toml`-supported settings that are provided on the command-line (e.g., via
3. If no `pyproject.toml` file is found in the filesystem hierarchy, Ruff will fall back to using
a default configuration. If a user-specific configuration file exists
at `${config_dir}/ruff/pyproject.toml`,
that file will be used instead of the default configuration, with `${config_dir}` being
determined via the [`dirs](https://docs.rs/dirs/4.0.0/dirs/fn.config_dir.html) crate, and all
relative paths being again resolved relative to the _current working directory_.
4. Any `pyproject.toml`-supported settings that are provided on the command-line (e.g., via
`--select`) will override the settings in _every_ resolved configuration file.
Unlike [ESLint](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#cascading-and-hierarchy),

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.181"
version = "0.0.182"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.181"
version = "0.0.182"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.181-dev.0"
version = "0.0.182-dev.0"
edition = "2021"
[lib]

View File

@@ -10,14 +10,14 @@ Running from the repo root should pick up and enforce the appropriate settings f
```
∴ cargo run resources/test/project/
Found 7 error(s).
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `os` imported but unused
resources/test/project/examples/.dotfiles/script.py:5:5: F841 Local variable `x` is assigned to but never used
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
resources/test/project/src/file.py:1:8: F401 `os` imported but unused
resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
4 potentially fixable with the --fix option.
6 potentially fixable with the --fix option.
```
Running from the project directory itself should exhibit the same behavior:
@@ -25,14 +25,14 @@ Running from the project directory itself should exhibit the same behavior:
```
∴ (cd resources/test/project/ && cargo run .)
Found 7 error(s).
examples/.dotfiles/script.py:1:8: F401 `os` imported but unused
examples/.dotfiles/script.py:5:5: F841 Local variable `x` is assigned to but never used
examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
src/file.py:1:8: F401 `os` imported but unused
src/file.py:5:5: F841 Local variable `x` is assigned to but never used
src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
4 potentially fixable with the --fix option.
6 potentially fixable with the --fix option.
```
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
@@ -52,18 +52,18 @@ file paths from the current working directory:
```
∴ (cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/)
Found 11 error(s).
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `os` imported but unused
resources/test/project/examples/.dotfiles/script.py:5:5: F841 Local variable `x` is assigned to but never used
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
resources/test/project/examples/docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused
resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
resources/test/project/src/file.py:1:8: F401 `os` imported but unused
resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
7 potentially fixable with the --fix option.
11 potentially fixable with the --fix option.
```
Running from a parent directory should this "ignore" the `exclude` (hence, `concepts/file.py` gets
@@ -72,10 +72,10 @@ included in the output):
```
∴ (cd resources/test/project/examples && cargo run -- --config=docs/pyproject.toml .)
Found 4 error(s).
.dotfiles/script.py:5:5: F841 Local variable `x` is assigned to but never used
docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used
1 potentially fixable with the --fix option.
```
@@ -83,8 +83,7 @@ Passing an excluded directory directly should report errors in the contained fil
```
∴ cargo run resources/test/project/examples/excluded/
Found 2 error(s).
Found 1 error(s).
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
resources/test/project/examples/excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used
1 potentially fixable with the --fix option.
```

View File

@@ -1,5 +1,2 @@
import os
def f():
x = 1
import numpy as np
from app import app_file

View File

@@ -1,6 +1,7 @@
[tool.ruff]
extend = "../../pyproject.toml"
src = ["."]
extend-select = ["I001"]
# Enable I001, and re-enable F841, to test extension priority.
extend-select = ["I001", "F841"]
extend-ignore = ["F401"]
extend-exclude = ["./docs/concepts/file.py"]

View File

@@ -2,3 +2,4 @@
src = [".", "python_modules/*"]
exclude = ["examples/excluded"]
extend-select = ["I001"]
extend-ignore = ["F841"]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.181"
version = "0.0.182"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.181"
version = "0.0.182"
edition = "2021"
[lib]

View File

@@ -1,9 +1,12 @@
use log::error;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{
Arguments, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, Stmt, StmtKind,
};
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use crate::ast::types::Range;
use crate::SourceCodeLocator;
@@ -311,13 +314,42 @@ pub fn count_trailing_lines(stmt: &Stmt, locator: &SourceCodeLocator) -> usize {
.count()
}
/// Return the appropriate visual `Range` for any message that spans a `Stmt`.
/// Specifically, this method returns the range of a function or class name,
/// rather than that of the entire function or class body.
pub fn identifier_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Range {
if matches!(
stmt.node,
StmtKind::ClassDef { .. }
| StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
) {
let contents = locator.slice_source_code_range(&Range::from_located(stmt));
for (start, tok, end) in lexer::make_tokenizer(&contents).flatten() {
if matches!(tok, Tok::Name { .. }) {
let start = to_absolute(start, stmt.location);
let end = to_absolute(end, stmt.location);
return Range {
location: start,
end_location: end,
};
}
}
error!("Failed to find identifier for {:?}", stmt);
}
Range::from_located(stmt)
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Location;
use rustpython_parser::parser;
use crate::ast::helpers::match_module_member;
use crate::ast::helpers::{identifier_range, match_module_member};
use crate::ast::types::Range;
use crate::source_code_locator::SourceCodeLocator;
#[test]
fn builtin() -> Result<()> {
@@ -461,4 +493,91 @@ mod tests {
));
Ok(())
}
#[test]
fn extract_identifier_range() -> Result<()> {
let contents = "def f(): pass".trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 4),
end_location: Location::new(1, 5),
}
);
let contents = r#"
def \
f():
pass
"#
.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(2, 2),
end_location: Location::new(2, 3),
}
);
let contents = "class Class(): pass".trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 6),
end_location: Location::new(1, 11),
}
);
let contents = "class Class: pass".trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 6),
end_location: Location::new(1, 11),
}
);
let contents = r#"
@decorator()
class Class():
pass
"#
.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(2, 6),
end_location: Location::new(2, 11),
}
);
let contents = r#"x = y + 1"#.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 0),
end_location: Location::new(1, 9),
}
);
Ok(())
}
}

View File

@@ -6,7 +6,7 @@ use itertools::Itertools;
use log::error;
use nohash_hasher::IntMap;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Location;
use rustpython_ast::{Located, Location};
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Operator, Stmt, StmtKind, Suite,
@@ -257,12 +257,11 @@ where
}
if self.settings.enabled.contains(&CheckCode::E741) {
let location = Range::from_located(stmt);
self.add_checks(
names
.iter()
.filter_map(|name| {
pycodestyle::checks::ambiguous_variable_name(name, location)
pycodestyle::checks::ambiguous_variable_name(name, stmt)
})
.into_iter(),
);
@@ -309,12 +308,11 @@ where
}
if self.settings.enabled.contains(&CheckCode::E741) {
let location = Range::from_located(stmt);
self.add_checks(
names
.iter()
.filter_map(|name| {
pycodestyle::checks::ambiguous_variable_name(name, location)
pycodestyle::checks::ambiguous_variable_name(name, stmt)
})
.into_iter(),
);
@@ -369,7 +367,8 @@ where
if let Some(check) = pep8_naming::checks::invalid_function_name(
stmt,
name,
&self.settings.pep8_naming,
&self.settings.pep8_naming.ignore_names,
self.locator,
) {
self.add_check(check);
}
@@ -406,9 +405,12 @@ where
}
if self.settings.enabled.contains(&CheckCode::N807) {
if let Some(check) =
pep8_naming::checks::dunder_function_name(self.current_scope(), stmt, name)
{
if let Some(check) = pep8_naming::checks::dunder_function_name(
self.current_scope(),
stmt,
name,
self.locator,
) {
self.add_check(check);
}
}
@@ -445,6 +447,7 @@ where
name,
body,
self.settings.mccabe.max_complexity,
self.locator,
) {
self.add_check(check);
}
@@ -460,7 +463,7 @@ where
pylint::plugins::property_with_parameters(self, stmt, decorator_list, args);
}
self.check_builtin_shadowing(name, Range::from_located(stmt), true);
self.check_builtin_shadowing(name, stmt, true);
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
@@ -548,7 +551,9 @@ where
}
if self.settings.enabled.contains(&CheckCode::N801) {
if let Some(check) = pep8_naming::checks::invalid_class_name(stmt, name) {
if let Some(check) =
pep8_naming::checks::invalid_class_name(stmt, name, self.locator)
{
self.add_check(check);
}
}
@@ -573,7 +578,7 @@ where
);
}
self.check_builtin_shadowing(name, Range::from_located(stmt), false);
self.check_builtin_shadowing(name, stmt, false);
for expr in bases {
self.visit_expr(expr);
@@ -615,7 +620,7 @@ where
);
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, Range::from_located(stmt), false);
self.check_builtin_shadowing(asname, stmt, false);
}
// Given `import foo`, `name` and `full_name` would both be `foo`.
@@ -683,7 +688,10 @@ where
if self.settings.enabled.contains(&CheckCode::N811) {
if let Some(check) =
pep8_naming::checks::constant_imported_as_non_constant(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -693,7 +701,10 @@ where
if self.settings.enabled.contains(&CheckCode::N812) {
if let Some(check) =
pep8_naming::checks::lowercase_imported_as_non_lowercase(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -703,7 +714,10 @@ where
if self.settings.enabled.contains(&CheckCode::N813) {
if let Some(check) =
pep8_naming::checks::camelcase_imported_as_lowercase(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -712,7 +726,10 @@ where
if self.settings.enabled.contains(&CheckCode::N814) {
if let Some(check) = pep8_naming::checks::camelcase_imported_as_constant(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
) {
self.add_check(check);
}
@@ -720,7 +737,10 @@ where
if self.settings.enabled.contains(&CheckCode::N817) {
if let Some(check) = pep8_naming::checks::camelcase_imported_as_acronym(
stmt, name, asname,
stmt,
name,
asname,
self.locator,
) {
self.add_check(check);
}
@@ -858,7 +878,7 @@ where
scope.import_starred = true;
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, Range::from_located(stmt), false);
self.check_builtin_shadowing(asname, stmt, false);
}
// Given `from foo import bar`, `name` would be "bar" and `full_name` would
@@ -927,6 +947,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -939,6 +960,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -951,6 +973,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
)
{
self.add_check(check);
@@ -962,6 +985,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
) {
self.add_check(check);
}
@@ -972,6 +996,7 @@ where
stmt,
&alias.node.name,
asname,
self.locator,
) {
self.add_check(check);
}
@@ -1422,15 +1447,14 @@ where
}
ExprContext::Store => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
id,
Range::from_located(expr),
) {
if let Some(check) =
pycodestyle::checks::ambiguous_variable_name(id, expr)
{
self.add_check(check);
}
}
self.check_builtin_shadowing(id, Range::from_located(expr), true);
self.check_builtin_shadowing(id, expr, true);
self.handle_node_store(id, expr);
}
@@ -2413,19 +2437,14 @@ where
match name {
Some(name) => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
name,
Range::from_located(excepthandler),
) {
if let Some(check) =
pycodestyle::checks::ambiguous_variable_name(name, excepthandler)
{
self.add_check(check);
}
}
self.check_builtin_shadowing(
name,
Range::from_located(excepthandler),
false,
);
self.check_builtin_shadowing(name, excepthandler, false);
if self.current_scope().values.contains_key(&name.as_str()) {
self.handle_node_store(
@@ -2538,23 +2557,18 @@ where
);
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
&arg.node.arg,
Range::from_located(arg),
) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(&arg.node.arg, arg) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::N803) {
if let Some(check) =
pep8_naming::checks::invalid_argument_name(&arg.node.arg, Range::from_located(arg))
{
if let Some(check) = pep8_naming::checks::invalid_argument_name(&arg.node.arg, arg) {
self.add_check(check);
}
}
self.check_builtin_arg_shadowing(&arg.node.arg, Range::from_located(arg));
self.check_builtin_arg_shadowing(&arg.node.arg, arg);
}
}
@@ -3584,12 +3598,12 @@ impl<'a> Checker<'a> {
}
}
fn check_builtin_shadowing(&mut self, name: &str, location: Range, is_attribute: bool) {
fn check_builtin_shadowing<T>(&mut self, name: &str, located: &Located<T>, is_attribute: bool) {
if is_attribute && matches!(self.current_scope().kind, ScopeKind::Class(_)) {
if self.settings.enabled.contains(&CheckCode::A003) {
if let Some(check) = flake8_builtins::checks::builtin_shadowing(
name,
location,
located,
flake8_builtins::types::ShadowingType::Attribute,
) {
self.add_check(check);
@@ -3599,7 +3613,7 @@ impl<'a> Checker<'a> {
if self.settings.enabled.contains(&CheckCode::A001) {
if let Some(check) = flake8_builtins::checks::builtin_shadowing(
name,
location,
located,
flake8_builtins::types::ShadowingType::Variable,
) {
self.add_check(check);
@@ -3608,11 +3622,11 @@ impl<'a> Checker<'a> {
}
}
fn check_builtin_arg_shadowing(&mut self, name: &str, location: Range) {
fn check_builtin_arg_shadowing(&mut self, name: &str, arg: &Arg) {
if self.settings.enabled.contains(&CheckCode::A002) {
if let Some(check) = flake8_builtins::checks::builtin_shadowing(
name,
location,
arg,
flake8_builtins::types::ShadowingType::Argument,
) {
self.add_check(check);

View File

@@ -1,10 +1,16 @@
use rustpython_ast::Located;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_builtins::types::ShadowingType;
use crate::python::builtins::BUILTINS;
/// Check builtin name shadowing.
pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType) -> Option<Check> {
pub fn builtin_shadowing<T>(
name: &str,
located: &Located<T>,
node_type: ShadowingType,
) -> Option<Check> {
if BUILTINS.contains(&name) {
Some(Check::new(
match node_type {
@@ -12,7 +18,7 @@ pub fn builtin_shadowing(name: &str, location: Range, node_type: ShadowingType)
ShadowingType::Argument => CheckKind::BuiltinArgumentShadowing(name.to_string()),
ShadowingType::Attribute => CheckKind::BuiltinAttributeShadowing(name.to_string()),
},
location,
Range::from_located(located),
))
} else {
None

View File

@@ -87,7 +87,7 @@ pub mod visibility;
/// Load the relevant `Settings` for a given `Path`.
fn resolve(path: &Path) -> Result<Settings> {
if let Some(pyproject) = pyproject::find_pyproject_toml(path) {
if let Some(pyproject) = pyproject::find_pyproject_toml(path)? {
// First priority: `pyproject.toml` in the current `Path`.
resolver::resolve_settings(&pyproject, &Relativity::Parent, None)
} else if let Some(pyproject) = pyproject::find_user_pyproject_toml() {

View File

@@ -44,7 +44,7 @@ fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<PyprojectDi
// current working directory. (This matches ESLint's behavior.)
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = pyproject::find_pyproject_toml(path_dedot::CWD.as_path()) {
} 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`

View File

@@ -1,7 +1,8 @@
use rustpython_ast::{ExcepthandlerKind, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::helpers::identifier_range;
use crate::checks::{Check, CheckKind};
use crate::source_code_locator::SourceCodeLocator;
fn get_complexity_number(stmts: &[Stmt]) -> usize {
let mut complexity = 0;
@@ -59,12 +60,13 @@ pub fn function_is_too_complex(
name: &str,
body: &[Stmt],
max_complexity: usize,
locator: &SourceCodeLocator,
) -> Option<Check> {
let complexity = get_complexity_number(body) + 1;
if complexity > max_complexity {
Some(Check::new(
CheckKind::FunctionIsTooComplex(name.to_string(), complexity),
Range::from_located(stmt),
identifier_range(stmt, locator),
))
} else {
None

View File

@@ -8,10 +8,10 @@ expression: checks
- 1
location:
row: 2
column: 0
column: 4
end_location:
row: 3
column: 8
row: 2
column: 11
fix: ~
- kind:
FunctionIsTooComplex:
@@ -19,10 +19,10 @@ expression: checks
- 1
location:
row: 7
column: 0
column: 4
end_location:
row: 8
column: 10
row: 7
column: 21
fix: ~
- kind:
FunctionIsTooComplex:
@@ -30,10 +30,10 @@ expression: checks
- 1
location:
row: 12
column: 0
column: 4
end_location:
row: 15
column: 12
row: 12
column: 14
fix: ~
- kind:
FunctionIsTooComplex:
@@ -41,10 +41,10 @@ expression: checks
- 3
location:
row: 19
column: 0
column: 4
end_location:
row: 25
column: 47
row: 19
column: 26
fix: ~
- kind:
FunctionIsTooComplex:
@@ -52,10 +52,10 @@ expression: checks
- 3
location:
row: 29
column: 0
column: 4
end_location:
row: 36
column: 47
row: 29
column: 14
fix: ~
- kind:
FunctionIsTooComplex:
@@ -63,10 +63,10 @@ expression: checks
- 2
location:
row: 40
column: 0
column: 4
end_location:
row: 42
column: 16
row: 40
column: 12
fix: ~
- kind:
FunctionIsTooComplex:
@@ -74,10 +74,10 @@ expression: checks
- 2
location:
row: 46
column: 0
column: 4
end_location:
row: 50
column: 19
row: 46
column: 12
fix: ~
- kind:
FunctionIsTooComplex:
@@ -85,10 +85,10 @@ expression: checks
- 2
location:
row: 54
column: 0
column: 4
end_location:
row: 58
column: 16
row: 54
column: 13
fix: ~
- kind:
FunctionIsTooComplex:
@@ -96,10 +96,10 @@ expression: checks
- 3
location:
row: 62
column: 0
column: 4
end_location:
row: 69
column: 7
row: 62
column: 20
fix: ~
- kind:
FunctionIsTooComplex:
@@ -107,10 +107,10 @@ expression: checks
- 2
location:
row: 63
column: 4
column: 8
end_location:
row: 67
column: 11
row: 63
column: 9
fix: ~
- kind:
FunctionIsTooComplex:
@@ -118,10 +118,10 @@ expression: checks
- 1
location:
row: 64
column: 8
column: 12
end_location:
row: 65
column: 16
row: 64
column: 13
fix: ~
- kind:
FunctionIsTooComplex:
@@ -129,10 +129,10 @@ expression: checks
- 4
location:
row: 73
column: 0
column: 4
end_location:
row: 81
column: 16
row: 73
column: 12
fix: ~
- kind:
FunctionIsTooComplex:
@@ -140,10 +140,10 @@ expression: checks
- 3
location:
row: 85
column: 0
column: 4
end_location:
row: 92
column: 16
row: 85
column: 22
fix: ~
- kind:
FunctionIsTooComplex:
@@ -151,10 +151,10 @@ expression: checks
- 3
location:
row: 96
column: 0
column: 10
end_location:
row: 103
column: 12
row: 96
column: 16
fix: ~
- kind:
FunctionIsTooComplex:
@@ -162,10 +162,10 @@ expression: checks
- 1
location:
row: 107
column: 0
column: 4
end_location:
row: 108
column: 17
row: 107
column: 20
fix: ~
- kind:
FunctionIsTooComplex:
@@ -173,10 +173,10 @@ expression: checks
- 9
location:
row: 113
column: 4
column: 8
end_location:
row: 138
column: 40
row: 113
column: 14
fix: ~
- kind:
FunctionIsTooComplex:
@@ -184,10 +184,10 @@ expression: checks
- 1
location:
row: 118
column: 12
column: 16
end_location:
row: 119
column: 20
row: 118
column: 17
fix: ~
- kind:
FunctionIsTooComplex:
@@ -195,10 +195,10 @@ expression: checks
- 2
location:
row: 121
column: 12
column: 16
end_location:
row: 123
column: 24
row: 121
column: 17
fix: ~
- kind:
FunctionIsTooComplex:
@@ -206,10 +206,10 @@ expression: checks
- 1
location:
row: 126
column: 12
column: 16
end_location:
row: 127
column: 20
row: 126
column: 17
fix: ~
- kind:
FunctionIsTooComplex:
@@ -217,10 +217,10 @@ expression: checks
- 1
location:
row: 129
column: 12
column: 16
end_location:
row: 130
column: 20
row: 129
column: 21
fix: ~
- kind:
FunctionIsTooComplex:
@@ -228,9 +228,9 @@ expression: checks
- 1
location:
row: 132
column: 12
column: 16
end_location:
row: 133
row: 132
column: 20
fix: ~
- kind:
@@ -239,9 +239,9 @@ expression: checks
- 1
location:
row: 135
column: 12
column: 16
end_location:
row: 136
column: 20
row: 135
column: 25
fix: ~

View File

@@ -8,10 +8,10 @@ expression: checks
- 4
location:
row: 73
column: 0
column: 4
end_location:
row: 81
column: 16
row: 73
column: 12
fix: ~
- kind:
FunctionIsTooComplex:
@@ -19,9 +19,9 @@ expression: checks
- 9
location:
row: 113
column: 4
column: 8
end_location:
row: 138
column: 40
row: 113
column: 14
fix: ~

View File

@@ -1,47 +1,53 @@
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use rustpython_ast::{Arg, Arguments, Expr, ExprKind, Stmt};
use crate::ast::function_type;
use crate::ast::helpers::identifier_range;
use crate::ast::types::{Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers;
use crate::pep8_naming::settings::Settings;
use crate::python::string::{self};
use crate::source_code_locator::SourceCodeLocator;
/// N801
pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
pub fn invalid_class_name(
class_def: &Stmt,
name: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
let stripped = name.strip_prefix('_').unwrap_or(name);
if !stripped.chars().next().map_or(false, char::is_uppercase) || stripped.contains('_') {
return Some(Check::new(
CheckKind::InvalidClassName(name.to_string()),
Range::from_located(class_def),
identifier_range(class_def, locator),
));
}
None
}
/// N802
pub fn invalid_function_name(func_def: &Stmt, name: &str, settings: &Settings) -> Option<Check> {
if name.to_lowercase() != name
&& !settings
.ignore_names
.iter()
.any(|ignore_name| ignore_name == name)
{
pub fn invalid_function_name(
func_def: &Stmt,
name: &str,
ignore_names: &[String],
locator: &SourceCodeLocator,
) -> Option<Check> {
if name.to_lowercase() != name && !ignore_names.iter().any(|ignore_name| ignore_name == name) {
return Some(Check::new(
CheckKind::InvalidFunctionName(name.to_string()),
Range::from_located(func_def),
identifier_range(func_def, locator),
));
}
None
}
/// N803
pub fn invalid_argument_name(name: &str, location: Range) -> Option<Check> {
pub fn invalid_argument_name(name: &str, arg: &Arg) -> Option<Check> {
if name.to_lowercase() != name {
return Some(Check::new(
CheckKind::InvalidArgumentName(name.to_string()),
location,
Range::from_located(arg),
));
}
None
@@ -124,7 +130,12 @@ pub fn invalid_first_argument_name_for_method(
}
/// N807
pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Check> {
pub fn dunder_function_name(
scope: &Scope,
stmt: &Stmt,
name: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
@@ -138,7 +149,7 @@ pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Ch
Some(Check::new(
CheckKind::DunderFunctionName,
Range::from_located(stmt),
identifier_range(stmt, locator),
))
}
@@ -147,11 +158,12 @@ pub fn constant_imported_as_non_constant(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if string::is_upper(name) && !string::is_upper(asname) {
return Some(Check::new(
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None
@@ -162,11 +174,12 @@ pub fn lowercase_imported_as_non_lowercase(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if !string::is_upper(name) && string::is_lower(name) && asname.to_lowercase() != asname {
return Some(Check::new(
CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None
@@ -177,11 +190,12 @@ pub fn camelcase_imported_as_lowercase(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if helpers::is_camelcase(name) && string::is_lower(asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None
@@ -192,6 +206,7 @@ pub fn camelcase_imported_as_constant(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if helpers::is_camelcase(name)
&& !string::is_lower(asname)
@@ -200,7 +215,7 @@ pub fn camelcase_imported_as_constant(
{
return Some(Check::new(
CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None
@@ -211,6 +226,7 @@ pub fn camelcase_imported_as_acronym(
import_from: &Stmt,
name: &str,
asname: &str,
locator: &SourceCodeLocator,
) -> Option<Check> {
if helpers::is_camelcase(name)
&& !string::is_lower(asname)
@@ -219,7 +235,7 @@ pub fn camelcase_imported_as_acronym(
{
return Some(Check::new(
CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()),
Range::from_located(import_from),
identifier_range(import_from, locator),
));
}
None

View File

@@ -6,45 +6,45 @@ expression: checks
InvalidClassName: bad
location:
row: 1
column: 0
column: 6
end_location:
row: 2
column: 8
row: 1
column: 9
fix: ~
- kind:
InvalidClassName: _bad
location:
row: 5
column: 0
column: 6
end_location:
row: 6
column: 8
row: 5
column: 10
fix: ~
- kind:
InvalidClassName: bad_class
location:
row: 9
column: 0
column: 6
end_location:
row: 10
column: 8
row: 9
column: 15
fix: ~
- kind:
InvalidClassName: Bad_Class
location:
row: 13
column: 0
column: 6
end_location:
row: 14
column: 8
row: 13
column: 15
fix: ~
- kind:
InvalidClassName: BAD_CLASS
location:
row: 17
column: 0
column: 6
end_location:
row: 18
column: 8
row: 17
column: 15
fix: ~

View File

@@ -6,45 +6,45 @@ expression: checks
InvalidFunctionName: Bad
location:
row: 4
column: 0
column: 4
end_location:
row: 5
column: 8
row: 4
column: 7
fix: ~
- kind:
InvalidFunctionName: _Bad
location:
row: 8
column: 0
column: 4
end_location:
row: 9
row: 8
column: 8
fix: ~
- kind:
InvalidFunctionName: BAD
location:
row: 12
column: 0
column: 4
end_location:
row: 13
column: 8
row: 12
column: 7
fix: ~
- kind:
InvalidFunctionName: BAD_FUNC
location:
row: 16
column: 0
column: 4
end_location:
row: 17
column: 8
row: 16
column: 12
fix: ~
- kind:
InvalidFunctionName: testTest
location:
row: 40
column: 4
column: 8
end_location:
row: 41
column: 19
row: 40
column: 16
fix: ~

View File

@@ -5,17 +5,17 @@ expression: checks
- kind: DunderFunctionName
location:
row: 1
column: 0
column: 4
end_location:
row: 2
column: 8
row: 1
column: 11
fix: ~
- kind: DunderFunctionName
location:
row: 14
column: 4
column: 8
end_location:
row: 15
column: 12
row: 14
column: 15
fix: ~

View File

@@ -1,5 +1,5 @@
use itertools::izip;
use rustpython_ast::{Location, Stmt, StmtKind};
use rustpython_ast::{Located, Location, Stmt, StmtKind};
use rustpython_parser::ast::{Cmpop, Expr, ExprKind};
use crate::ast::types::Range;
@@ -65,11 +65,11 @@ fn is_ambiguous_name(name: &str) -> bool {
}
/// E741
pub fn ambiguous_variable_name(name: &str, location: Range) -> Option<Check> {
pub fn ambiguous_variable_name<T>(name: &str, located: &Located<T>) -> Option<Check> {
if is_ambiguous_name(name) {
Some(Check::new(
CheckKind::AmbiguousVariableName(name.to_string()),
location,
Range::from_located(located),
))
} else {
None

View File

@@ -14,6 +14,7 @@ use rustc_hash::FxHashSet;
use crate::cli::Overrides;
use crate::fs;
use crate::settings::configuration::Configuration;
use crate::settings::pyproject::has_ruff_section;
use crate::settings::{pyproject, Settings};
/// The strategy used to discover Python files in the filesystem..
@@ -201,9 +202,11 @@ pub fn python_files_in_path(
for ancestor in path.ancestors() {
let pyproject = ancestor.join("pyproject.toml");
if pyproject.is_file() {
let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
resolver.add(root, settings);
if has_ruff_section(&pyproject)? {
let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
resolver.add(root, settings);
}
}
}
}
@@ -237,12 +240,23 @@ pub fn python_files_in_path(
{
let pyproject = entry.path().join("pyproject.toml");
if pyproject.is_file() {
match resolve_scoped_settings(
&pyproject,
&Relativity::Parent,
Some(overrides),
) {
Ok((root, settings)) => resolver.write().unwrap().add(root, settings),
match has_ruff_section(&pyproject) {
Ok(false) => {}
Ok(true) => {
match resolve_scoped_settings(
&pyproject,
&Relativity::Parent,
Some(overrides),
) {
Ok((root, settings)) => {
resolver.write().unwrap().add(root, settings);
}
Err(err) => {
*error.lock().unwrap() = Err(err);
return WalkState::Quit;
}
}
}
Err(err) => {
*error.lock().unwrap() = Err(err);
return WalkState::Quit;
@@ -272,8 +286,8 @@ pub fn python_files_in_path(
return WalkState::Skip;
}
}
Err(e) => {
debug!("Ignored path due to error in parsing: {:?}: {}", path, e);
Err(err) => {
debug!("Ignored path due to error in parsing: {:?}: {}", path, err);
return WalkState::Skip;
}
}

View File

@@ -24,9 +24,9 @@ pub struct Configuration {
pub dummy_variable_rgx: Option<Regex>,
pub exclude: Option<Vec<FilePattern>>,
pub extend: Option<PathBuf>,
pub extend_exclude: Option<Vec<FilePattern>>,
pub extend_ignore: Option<Vec<CheckCodePrefix>>,
pub extend_select: Option<Vec<CheckCodePrefix>>,
pub extend_exclude: Vec<FilePattern>,
pub extend_ignore: Vec<Vec<CheckCodePrefix>>,
pub extend_select: Vec<Vec<CheckCodePrefix>>,
pub external: Option<Vec<String>>,
pub fix: Option<bool>,
pub fixable: Option<Vec<CheckCodePrefix>>,
@@ -60,18 +60,12 @@ impl Configuration {
pub fn from_options(options: Options, project_root: &Path) -> Result<Self> {
Ok(Configuration {
extend: options.extend.map(PathBuf::from),
allowed_confusables: options.allowed_confusables,
dummy_variable_rgx: options
.dummy_variable_rgx
.map(|pattern| Regex::new(&pattern))
.transpose()
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
src: options
.src
.map(|src| resolve_src(&src, project_root))
.transpose()?,
target_version: options.target_version,
exclude: options.exclude.map(|paths| {
paths
.into_iter()
@@ -81,22 +75,24 @@ impl Configuration {
})
.collect()
}),
extend_exclude: options.extend_exclude.map(|paths| {
paths
.into_iter()
.map(|pattern| {
let absolute = fs::normalize_path_to(Path::new(&pattern), project_root);
FilePattern::User(pattern, absolute)
})
.collect()
}),
extend_ignore: options.extend_ignore,
select: options.select,
extend_select: options.extend_select,
extend: options.extend.map(PathBuf::from),
extend_exclude: options
.extend_exclude
.map(|paths| {
paths
.into_iter()
.map(|pattern| {
let absolute = fs::normalize_path_to(Path::new(&pattern), project_root);
FilePattern::User(pattern, absolute)
})
.collect()
})
.unwrap_or_default(),
extend_ignore: vec![options.extend_ignore.unwrap_or_default()],
extend_select: vec![options.extend_select.unwrap_or_default()],
external: options.external,
fix: options.fix,
fixable: options.fixable,
unfixable: options.unfixable,
format: options.format,
ignore: options.ignore,
ignore_init_module_imports: options.ignore_init_module_imports,
@@ -111,7 +107,14 @@ impl Configuration {
.collect()
}),
respect_gitignore: options.respect_gitignore,
select: options.select,
show_source: options.show_source,
src: options
.src
.map(|src| resolve_src(&src, project_root))
.transpose()?,
target_version: options.target_version,
unfixable: options.unfixable,
// Plugins
flake8_annotations: options.flake8_annotations,
flake8_bugbear: options.flake8_bugbear,
@@ -133,9 +136,21 @@ impl Configuration {
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),
extend_select: self.extend_select.or(config.extend_select),
extend_exclude: config
.extend_exclude
.into_iter()
.chain(self.extend_exclude.into_iter())
.collect(),
extend_ignore: config
.extend_ignore
.into_iter()
.chain(self.extend_ignore.into_iter())
.collect(),
extend_select: config
.extend_select
.into_iter()
.chain(self.extend_select.into_iter())
.collect(),
external: self.external.or(config.external),
fix: self.fix.or(config.fix),
fixable: self.fixable.or(config.fixable),
@@ -174,13 +189,7 @@ impl Configuration {
self.exclude = Some(exclude);
}
if let Some(extend_exclude) = overrides.extend_exclude {
self.extend_exclude = Some(extend_exclude);
}
if let Some(extend_ignore) = overrides.extend_ignore {
self.extend_ignore = Some(extend_ignore);
}
if let Some(extend_select) = overrides.extend_select {
self.extend_select = Some(extend_select);
self.extend_exclude.extend(extend_exclude);
}
if let Some(fix) = overrides.fix {
self.fix = Some(fix);
@@ -220,6 +229,23 @@ impl Configuration {
if let Some(unfixable) = overrides.unfixable {
self.unfixable = Some(unfixable);
}
// Special-case: `extend_ignore` and `extend_select` are parallel arrays, so
// push an empty array if only one of the two is provided.
match (overrides.extend_ignore, overrides.extend_select) {
(Some(extend_ignore), Some(extend_select)) => {
self.extend_ignore.push(extend_ignore);
self.extend_select.push(extend_select);
}
(Some(extend_ignore), None) => {
self.extend_ignore.push(extend_ignore);
self.extend_select.push(Vec::new());
}
(None, Some(extend_select)) => {
self.extend_ignore.push(Vec::new());
self.extend_select.push(extend_select);
}
(None, None) => {}
}
}
}

View File

@@ -97,26 +97,31 @@ impl Settings {
.dummy_variable_rgx
.unwrap_or_else(|| DEFAULT_DUMMY_VARIABLE_RGX.clone()),
enabled: resolve_codes(
&config
.select
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F])
.into_iter()
.chain(config.extend_select.unwrap_or_default().into_iter())
.collect::<Vec<_>>(),
&config
.ignore
.unwrap_or_default()
.into_iter()
.chain(config.extend_ignore.unwrap_or_default().into_iter())
.collect::<Vec<_>>(),
[CheckCodeSpec {
select: &config
.select
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]),
ignore: &config.ignore.unwrap_or_default(),
}]
.into_iter()
.chain(
config
.extend_select
.iter()
.zip(config.extend_ignore.iter())
.map(|(select, ignore)| CheckCodeSpec { select, ignore }),
),
),
exclude: resolve_globset(config.exclude.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()))?,
extend_exclude: resolve_globset(config.extend_exclude.unwrap_or_default())?,
extend_exclude: resolve_globset(config.extend_exclude)?,
external: FxHashSet::from_iter(config.external.unwrap_or_default()),
fix: config.fix.unwrap_or(false),
fixable: resolve_codes(
&config.fixable.unwrap_or_else(|| CATEGORIES.to_vec()),
&config.unfixable.unwrap_or_default(),
[CheckCodeSpec {
select: &config.fixable.unwrap_or_else(|| CATEGORIES.to_vec()),
ignore: &config.unfixable.unwrap_or_default(),
}]
.into_iter(),
),
format: config.format.unwrap_or(SerializationFormat::Text),
ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(),
@@ -301,26 +306,34 @@ pub fn resolve_per_file_ignores(
.collect()
}
#[derive(Debug)]
struct CheckCodeSpec<'a> {
select: &'a [CheckCodePrefix],
ignore: &'a [CheckCodePrefix],
}
/// Given a set of selected and ignored prefixes, resolve the set of enabled
/// error codes.
fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet<CheckCode> {
fn resolve_codes<'a>(specs: impl Iterator<Item = CheckCodeSpec<'a>>) -> FxHashSet<CheckCode> {
let mut codes: FxHashSet<CheckCode> = FxHashSet::default();
for specificity in [
SuffixLength::Zero,
SuffixLength::One,
SuffixLength::Two,
SuffixLength::Three,
SuffixLength::Four,
] {
for prefix in select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
for spec in specs {
for specificity in [
SuffixLength::Zero,
SuffixLength::One,
SuffixLength::Two,
SuffixLength::Three,
SuffixLength::Four,
] {
for prefix in spec.select {
if prefix.specificity() == specificity {
codes.extend(prefix.codes());
}
}
}
for prefix in ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
for prefix in spec.ignore {
if prefix.specificity() == specificity {
for code in prefix.codes() {
codes.remove(&code);
}
}
}
}
@@ -334,24 +347,80 @@ mod tests {
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::resolve_codes;
use crate::settings::{resolve_codes, CheckCodeSpec};
#[test]
fn resolver() {
let actual = resolve_codes(&[CheckCodePrefix::W], &[]);
fn check_codes() {
let actual = resolve_codes(
[CheckCodeSpec {
select: &[CheckCodePrefix::W],
ignore: &[],
}]
.into_iter(),
);
let expected = FxHashSet::from_iter([CheckCode::W292, CheckCode::W605]);
assert_eq!(actual, expected);
let actual = resolve_codes(&[CheckCodePrefix::W6], &[]);
let actual = resolve_codes(
[CheckCodeSpec {
select: &[CheckCodePrefix::W6],
ignore: &[],
}]
.into_iter(),
);
let expected = FxHashSet::from_iter([CheckCode::W605]);
assert_eq!(actual, expected);
let actual = resolve_codes(&[CheckCodePrefix::W], &[CheckCodePrefix::W292]);
let actual = resolve_codes(
[CheckCodeSpec {
select: &[CheckCodePrefix::W],
ignore: &[CheckCodePrefix::W292],
}]
.into_iter(),
);
let expected = FxHashSet::from_iter([CheckCode::W605]);
assert_eq!(actual, expected);
let actual = resolve_codes(&[CheckCodePrefix::W605], &[CheckCodePrefix::W605]);
let actual = resolve_codes(
[CheckCodeSpec {
select: &[CheckCodePrefix::W605],
ignore: &[CheckCodePrefix::W605],
}]
.into_iter(),
);
let expected = FxHashSet::from_iter([]);
assert_eq!(actual, expected);
let actual = resolve_codes(
[
CheckCodeSpec {
select: &[CheckCodePrefix::W],
ignore: &[CheckCodePrefix::W292],
},
CheckCodeSpec {
select: &[CheckCodePrefix::W292],
ignore: &[],
},
]
.into_iter(),
);
let expected = FxHashSet::from_iter([CheckCode::W292, CheckCode::W605]);
assert_eq!(actual, expected);
let actual = resolve_codes(
[
CheckCodeSpec {
select: &[CheckCodePrefix::W],
ignore: &[CheckCodePrefix::W292],
},
CheckCodeSpec {
select: &[CheckCodePrefix::W292],
ignore: &[CheckCodePrefix::W],
},
]
.into_iter(),
);
let expected = FxHashSet::from_iter([CheckCode::W292]);
assert_eq!(actual, expected);
}
}

View File

@@ -33,17 +33,24 @@ fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> {
toml::from_str(&contents).map_err(std::convert::Into::into)
}
/// Find the nearest `pyproject.toml` file.
pub fn find_pyproject_toml(path: &Path) -> Option<PathBuf> {
for directory in path.ancestors() {
let pyproject = directory.join("pyproject.toml");
if pyproject.is_file() {
return Some(pyproject);
}
}
None
/// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section.
pub fn has_ruff_section(path: &Path) -> Result<bool> {
let pyproject = parse_pyproject_toml(path)?;
Ok(pyproject.tool.and_then(|tool| tool.ruff).is_some())
}
/// Find the path to the `pyproject.toml` file, if such a file exists.
pub fn find_pyproject_toml(path: &Path) -> Result<Option<PathBuf>> {
for directory in path.ancestors() {
let pyproject = directory.join("pyproject.toml");
if pyproject.is_file() && has_ruff_section(&pyproject)? {
return Ok(Some(pyproject));
}
}
Ok(None)
}
/// Find the path to the user-specific `pyproject.toml`, if it exists.
pub fn find_user_pyproject_toml() -> Option<PathBuf> {
let mut path = dirs::config_dir()?;
path.push("ruff");
@@ -55,6 +62,7 @@ pub fn find_user_pyproject_toml() -> Option<PathBuf> {
}
}
/// Load `Options` from a `pyproject.toml`.
pub fn load_options(pyproject: &Path) -> Result<Options> {
Ok(parse_pyproject_toml(pyproject)
.map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))?
@@ -355,7 +363,7 @@ other-attribute = 1
fn find_and_parse_pyproject_toml() -> Result<()> {
let cwd = current_dir()?;
let pyproject =
find_pyproject_toml(&cwd.join("resources/test/fixtures/__init__.py")).unwrap();
find_pyproject_toml(&cwd.join("resources/test/fixtures/__init__.py"))?.unwrap();
assert_eq!(
pyproject,
cwd.join("resources/test/fixtures/pyproject.toml")