Compare commits

..

19 Commits

Author SHA1 Message Date
Charlie Marsh
6fef4db433 Bump version to 0.0.48 2022-09-29 16:40:01 -04:00
Charlie Marsh
7470d6832f Add pattern matching limitation to README.md 2022-09-29 16:39:25 -04:00
Nikita Sobolev
63ba0bfeef Adds flake8-builtins (#284) 2022-09-29 16:37:43 -04:00
Anders Kaseorg
91666fcaf6 Don’t follow directory symlinks found while walking (#280) 2022-09-29 15:10:25 -04:00
Heyward Fann
643e27221d chore: fix eslint fix link (#281) 2022-09-29 07:15:07 -04:00
Charlie Marsh
c7349b69c1 Bump version to 0.0.47 2022-09-28 22:30:48 -04:00
Charlie Marsh
7f84753f3c Improve rendering of --show-settings 2022-09-28 22:30:20 -04:00
Charlie Marsh
e2ec62cf33 Misc. follow-up changes to #272 (#278) 2022-09-28 22:15:58 -04:00
Charlie Marsh
1d5592d937 Use take-while to terminate on parse errors (#279) 2022-09-28 22:06:35 -04:00
Anders Kaseorg
886def13bd Upgrade to clap 4 (#272) 2022-09-28 17:11:57 -04:00
Charlie Marsh
949e4d4077 Bump version to 0.0.46 2022-09-24 13:10:10 -04:00
Charlie Marsh
c8cb2eead2 Remove README note about noqa patterns 2022-09-24 13:09:45 -04:00
Seamooo
02ae494a0e Enable per-file ignores (#261) 2022-09-24 13:02:34 -04:00
Harutaka Kawamura
dce86e065b Make unused variable pattern configurable (#265) 2022-09-24 10:43:39 -04:00
Harutaka Kawamura
d77979429c Print warning and error messages in stderr (#267) 2022-09-24 09:27:35 -04:00
Adrian Garcia Badaracco
a3a15d2eb2 error invalid pyproject.toml configs (#264) 2022-09-23 21:16:07 -04:00
Charlie Marsh
5af95428ff Tweak import 2022-09-23 18:53:57 -04:00
Harutaka Kawamura
6338cad4e6 Remove python 3.6 classifier (#260) 2022-09-22 20:38:09 -04:00
Harutaka Kawamura
485881877f Include error code and message in JSON output (#259) 2022-09-22 20:29:21 -04:00
26 changed files with 946 additions and 188 deletions

22
Cargo.lock generated
View File

@@ -382,26 +382,24 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "clap"
version = "3.2.16"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9"
checksum = "dd03107d0f87139c1774a15f3db2165b0652b5460c58c27e561f89c20c599eaf"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"once_cell",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.2.15"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba52acd3b0a5c33aeada5cdaa3267cdc7c594a98731d4268cdc1532f4264cb4"
checksum = "ca689d7434ce44517a12a89456b2be4d1ea1cafcd8f581978c03d45f5a5c12a7"
dependencies = [
"heck",
"proc-macro-error",
@@ -412,9 +410,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.2.4"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
dependencies = [
"os_str_bytes",
]
@@ -1801,7 +1799,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.45"
version = "0.0.48"
dependencies = [
"anyhow",
"bincode",
@@ -2187,12 +2185,6 @@ dependencies = [
"phf_codegen 0.8.0",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.32"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.45"
version = "0.0.48"
edition = "2021"
[lib]
@@ -11,7 +11,7 @@ anyhow = { version = "1.0.60" }
bincode = { version = "1.3.3" }
cacache = { version = "10.0.1" }
chrono = { version = "0.4.21" }
clap = { version = "3.2.16", features = ["derive"] }
clap = { version = "4.0.1", features = ["derive"] }
clearscreen = { version = "1.0.10" }
colored = { version = "2.0.0" }
common-path = { version = "1.0.0" }

View File

@@ -20,7 +20,7 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.10 compatibility
- 🛠️ `pyproject.toml` support
- 📦 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache support
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired `--fix` support
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix)-inspired `--fix` support
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` support
- ⚖️ [Near-complete parity](#Parity-with-Flake8) with the built-in Flake8 rule set
@@ -80,59 +80,60 @@ select = [
Alternatively, on the command-line:
```shell
ruff path/to/code/ --select F401 F403
ruff path/to/code/ --select F401 --select F403
```
See `ruff --help` for more:
```shell
ruff (v0.0.45) 0.0.45
An extremely fast Python linter.
ruff: An extremely fast Python linter.
USAGE:
ruff [OPTIONS] <FILES>...
Usage: ruff [OPTIONS] <FILES>...
ARGS:
<FILES>...
Arguments:
<FILES>...
OPTIONS:
--select <SELECT>...
List of error codes to enable
--extend-select <EXTEND_SELECT>...
Like --select, but adds additional error codes on top of the selected ones
--ignore <IGNORE>...
List of error codes to ignore
--extend-ignore <EXTEND_IGNORE>...
Like --ignore, but adds additional error codes on top of the ignored ones
--exclude <EXCLUDE>...
List of paths, used to exclude files and/or directories from checks
--extend-exclude <EXTEND_EXCLUDE>...
Like --exclude, but adds additional files and directories on top of the excluded ones
-e, --exit-zero
Exit with status code "0", even upon detecting errors
-f, --fix
Attempt to automatically fix lint errors
--format <FORMAT>
Output serialization format for error messages [default: text] [possible values: text,
json]
-h, --help
Print help information
-n, --no-cache
Disable cache reads
-q, --quiet
Disable all logging (but still exit with status code "1" upon detecting errors)
--add-noqa
Enable automatic additions of noqa directives to failing lines
--show-files
See the files ruff will be run against with the current settings
--show-settings
See ruff's settings
-v, --verbose
Enable verbose logging
-V, --version
Print version information
-w, --watch
Run in watch mode by re-running whenever files change
Options:
-v, --verbose
Enable verbose logging
-q, --quiet
Disable all logging (but still exit with status code "1" upon detecting errors)
-e, --exit-zero
Exit with status code "0", even upon detecting errors
-w, --watch
Run in watch mode by re-running whenever files change
-f, --fix
Attempt to automatically fix lint errors
-n, --no-cache
Disable cache reads
--select <SELECT>
List of error codes to enable
--extend-select <EXTEND_SELECT>
Like --select, but adds additional error codes on top of the selected ones
--ignore <IGNORE>
List of error codes to ignore
--extend-ignore <EXTEND_IGNORE>
Like --ignore, but adds additional error codes on top of the ignored ones
--exclude <EXCLUDE>
List of paths, used to exclude files and/or directories from checks
--extend-exclude <EXTEND_EXCLUDE>
Like --exclude, but adds additional files and directories on top of the excluded ones
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--format <FORMAT>
Output serialization format for error messages [default: text] [possible values: text, json]
--show-files
See the files ruff will be run against with the current settings
--show-settings
See ruff's settings
--add-noqa
Enable automatic additions of noqa directives to failing lines
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
-h, --help
Print help information
-V, --version
Print version information
```
### Excluding files
@@ -210,8 +211,8 @@ variables.)
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
1. Flake8 has a plugin architecture and supports writing custom lint rules.
2. Flake8 supports a wider range of `noqa` patterns, such as per-file ignores defined in `.flake8`.
3. ruff does not yet support parenthesized context managers.
2. ruff does not yet support a few Python 3.9 and 3.10 language features, including structural
pattern matching and parenthesized context managers.
## Rules

View File

@@ -2,14 +2,14 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, ValueHint};
use clap::Parser;
use rustpython_parser::parser;
use ruff::fs;
#[derive(Debug, Parser)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::FilePath, required = true)]
#[arg(required = true)]
file: PathBuf,
}

View File

@@ -2,14 +2,14 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::{Parser, ValueHint};
use clap::Parser;
use rustpython_parser::lexer;
use ruff::fs;
#[derive(Debug, Parser)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::FilePath, required = true)]
#[arg(required = true)]
file: PathBuf,
}

View File

@@ -8,7 +8,6 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",

27
resources/test/fixtures/A001.py vendored Normal file
View File

@@ -0,0 +1,27 @@
import some as sum
from some import other as int
print = 1
copyright: 'annotation' = 2
(complex := 3)
float = object = 4
min, max = 5, 6
def bytes():
pass
class slice:
pass
try:
...
except ImportError as ValueError:
...
for memoryview, *bytearray in []:
pass
with open('file') as str, open('file2') as (all, any):
pass
[0 for sum in ()]

9
resources/test/fixtures/A002.py vendored Normal file
View File

@@ -0,0 +1,9 @@
def func1(str, /, type, *complex, Exception, **getattr):
pass
async def func2(bytes):
pass
map([], lambda float: ...)

8
resources/test/fixtures/A003.py vendored Normal file
View File

@@ -0,0 +1,8 @@
class MyClass:
ImportError = 4
def __init__(self):
self.float = 5 # is fine
def str(self):
pass

View File

@@ -10,13 +10,13 @@ except ValueError as e:
print(e)
def f():
def f1():
x = 1
y = 2
z = x + y
def g():
def f2():
foo = (1, 2)
(a, b) = (1, 2)
@@ -26,6 +26,12 @@ def g():
(x, y) = baz = bar
def h():
def f3():
locals()
x = 1
def f4():
_ = 1
__ = 1
_discarded = 1

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeSet;
use itertools::izip;
use regex::Regex;
use rustpython_parser::ast::{
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword,
Location, Stmt, StmtKind, Unaryop,
@@ -10,6 +11,7 @@ use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{Binding, BindingKind, CheckLocator, FunctionScope, Scope, ScopeKind};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckKind, Fix, RejectedCmpop};
use crate::python::builtins::BUILTINS;
/// Check IfTuple compliance.
pub fn check_if_tuple(test: &Expr, location: Location) -> Option<Check> {
@@ -71,7 +73,11 @@ pub fn check_not_tests(
}
/// Check UnusedVariable compliance.
pub fn check_unused_variables(scope: &Scope, locator: &dyn CheckLocator) -> Vec<Check> {
pub fn check_unused_variables(
scope: &Scope,
locator: &dyn CheckLocator,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(
@@ -82,13 +88,12 @@ pub fn check_unused_variables(scope: &Scope, locator: &dyn CheckLocator) -> Vec<
}
for (name, binding) in scope.values.iter() {
// TODO(charlie): Ignore if using `locals`.
if binding.used.is_none()
&& name != "_"
&& matches!(binding.kind, BindingKind::Assignment)
&& !dummy_variable_rgx.is_match(name)
&& name != "__tracebackhide__"
&& name != "__traceback_info__"
&& name != "__traceback_supplement__"
&& matches!(binding.kind, BindingKind::Assignment)
{
checks.push(Check::new(
CheckKind::UnusedVariable(name.to_string()),
@@ -648,3 +653,30 @@ pub fn check_continue_outside_loop(
None
}
}
// flake8-builtins
pub enum ShadowingType {
Variable,
Argument,
Attribute,
}
/// Check builtin name shadowing
pub fn check_builtin_shadowing(
name: &str,
location: Location,
node_type: ShadowingType,
) -> Option<Check> {
if BUILTINS.contains(&name) {
Some(Check::new(
match node_type {
ShadowingType::Variable => CheckKind::BuiltinVariableShadowing(name.to_string()),
ShadowingType::Argument => CheckKind::BuiltinArgumentShadowing(name.to_string()),
ShadowingType::Attribute => CheckKind::BuiltinAttributeShadowing(name.to_string()),
},
location,
))
} else {
None
}
}

View File

@@ -246,6 +246,9 @@ where
self.checks.push(check);
}
}
self.check_builtin_shadowing(name, stmt.location, true);
for expr in decorator_list {
self.visit_expr(expr);
}
@@ -342,6 +345,8 @@ where
}
}
self.check_builtin_shadowing(name, self.locate_check(stmt.location), false);
for expr in bases {
self.visit_expr(expr)
}
@@ -382,6 +387,10 @@ where
},
)
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, stmt.location, false);
}
self.add_binding(
alias
.node
@@ -504,6 +513,10 @@ where
.expect("No current scope found."))];
scope.import_starred = true;
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, stmt.location, false);
}
let binding = Binding {
kind: BindingKind::Importation(match module {
None => name.clone(),
@@ -667,6 +680,9 @@ where
self.checks.push(check);
}
}
self.check_builtin_shadowing(id, expr.location, true);
let parent =
self.parents[*(self.parent_stack.last().expect("No parent found."))];
self.handle_node_store(expr, parent);
@@ -977,6 +993,9 @@ where
self.checks.push(check);
}
}
self.check_builtin_shadowing(name, excepthandler.location, false);
let scope = &self.scopes
[*(self.scope_stack.last().expect("No current scope found."))];
if scope.values.contains_key(name) {
@@ -1080,6 +1099,8 @@ where
self.checks.push(check);
}
}
self.check_builtin_arg_shadowing(&arg.node.arg, arg.location);
}
}
@@ -1415,8 +1436,11 @@ impl<'a> Checker<'a> {
fn check_deferred_assignments(&mut self) {
if self.settings.select.contains(&CheckCode::F841) {
while let Some(index) = self.deferred_assignments.pop() {
self.checks
.extend(checks::check_unused_variables(&self.scopes[index], self));
self.checks.extend(checks::check_unused_variables(
&self.scopes[index],
self,
&self.settings.dummy_variable_rgx,
));
}
}
}
@@ -1502,6 +1526,44 @@ impl<'a> Checker<'a> {
}
}
}
fn check_builtin_shadowing(&mut self, name: &str, location: Location, is_attribute: bool) {
let scope = &self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
// flake8-builtins
if is_attribute
&& matches!(scope.kind, ScopeKind::Class)
&& self.settings.select.contains(&CheckCode::A003)
{
if let Some(check) = checks::check_builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Attribute,
) {
self.checks.push(check);
}
} else if self.settings.select.contains(&CheckCode::A001) {
if let Some(check) = checks::check_builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Variable,
) {
self.checks.push(check);
}
}
}
fn check_builtin_arg_shadowing(&mut self, name: &str, location: Location) {
if self.settings.select.contains(&CheckCode::A002) {
if let Some(check) = checks::check_builtin_shadowing(
name,
self.locate_check(location),
checks::ShadowingType::Argument,
) {
self.checks.push(check);
}
}
}
}
pub fn check_ast(

View File

@@ -180,10 +180,11 @@ pub fn check_lines(
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::settings;
use super::check_lines;
use super::*;
#[test]
fn e501_non_ascii_char() {
@@ -191,19 +192,14 @@ mod tests {
let noqa_line_for: Vec<usize> = vec![1];
let check_with_max_line_length = |line_length: usize| {
let mut checks: Vec<Check> = vec![];
let settings = Settings {
pyproject: None,
project_root: None,
line_length,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from_iter(vec![CheckCode::E501]),
};
check_lines(
&mut checks,
line,
&noqa_line_for,
&settings,
&settings::Settings {
line_length,
..settings::Settings::for_rule(CheckCode::E501)
},
&fixer::Mode::Generate,
);
return checks;

View File

@@ -4,7 +4,7 @@ use anyhow::Result;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [
pub const DEFAULT_CHECK_CODES: [CheckCode; 45] = [
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
@@ -47,9 +47,13 @@ pub const DEFAULT_CHECK_CODES: [CheckCode; 42] = [
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
// flake8-builtins
CheckCode::A001,
CheckCode::A002,
CheckCode::A003,
];
pub const ALL_CHECK_CODES: [CheckCode; 45] = [
pub const ALL_CHECK_CODES: [CheckCode; 48] = [
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
@@ -95,6 +99,10 @@ pub const ALL_CHECK_CODES: [CheckCode; 45] = [
CheckCode::M001,
CheckCode::R001,
CheckCode::R002,
// flake8-builtins
CheckCode::A001,
CheckCode::A002,
CheckCode::A003,
];
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord)]
@@ -144,6 +152,10 @@ pub enum CheckCode {
R001,
R002,
M001,
// flake8-builtins
A001,
A002,
A003,
}
impl FromStr for CheckCode {
@@ -196,6 +208,10 @@ impl FromStr for CheckCode {
"R001" => Ok(CheckCode::R001),
"R002" => Ok(CheckCode::R002),
"M001" => Ok(CheckCode::M001),
// flake8-builtins
"A001" => Ok(CheckCode::A001),
"A002" => Ok(CheckCode::A002),
"A003" => Ok(CheckCode::A003),
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
}
}
@@ -249,6 +265,10 @@ impl CheckCode {
CheckCode::R001 => "R001",
CheckCode::R002 => "R002",
CheckCode::M001 => "M001",
// flake8-builtins
CheckCode::A001 => "A001",
CheckCode::A002 => "A002",
CheckCode::A003 => "A003",
}
}
@@ -256,7 +276,7 @@ impl CheckCode {
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 | CheckCode::M001 => &LintSource::Lines,
CheckCode::E902 | CheckCode::E999 => &LintSource::FileSystem,
CheckCode::E902 => &LintSource::FileSystem,
_ => &LintSource::AST,
}
}
@@ -309,6 +329,10 @@ impl CheckCode {
CheckCode::M001 => CheckKind::UnusedNOQA(None),
CheckCode::R001 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::R002 => CheckKind::NoAssertEquals,
// flake8-builtins
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
CheckCode::A003 => CheckKind::BuiltinAttributeShadowing("...".to_string()),
}
}
}
@@ -373,6 +397,10 @@ pub enum CheckKind {
UnusedVariable(String),
UselessObjectInheritance(String),
YieldOutsideFunction,
// flake8-builtin
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
}
impl CheckKind {
@@ -426,6 +454,10 @@ impl CheckKind {
CheckKind::UselessObjectInheritance(_) => "UselessObjectInheritance",
CheckKind::YieldOutsideFunction => "YieldOutsideFunction",
CheckKind::UnusedNOQA(_) => "UnusedNOQA",
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => "BuiltinVariableShadowing",
CheckKind::BuiltinArgumentShadowing(_) => "BuiltinArgumentShadowing",
CheckKind::BuiltinAttributeShadowing(_) => "BuiltinAttributeShadowing",
}
}
@@ -477,6 +509,10 @@ impl CheckKind {
CheckKind::UnusedVariable(_) => &CheckCode::F841,
CheckKind::UselessObjectInheritance(_) => &CheckCode::R001,
CheckKind::YieldOutsideFunction => &CheckCode::F704,
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
}
}
@@ -611,6 +647,16 @@ impl CheckKind {
None => "Unused `noqa` directive".to_string(),
Some(code) => format!("Unused `noqa` directive for: {code}"),
},
// flake8-builtins
CheckKind::BuiltinVariableShadowing(name) => {
format!("Variable `{name}` is shadowing a python builtin")
}
CheckKind::BuiltinArgumentShadowing(name) => {
format!("Argument `{name}` is shadowing a python builtin")
}
CheckKind::BuiltinAttributeShadowing(name) => {
format!("class attribute `{name}` is shadowing a python builtin")
}
}
}

View File

@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Deref;
@@ -10,7 +11,8 @@ use path_absolutize::path_dedot;
use path_absolutize::Absolutize;
use walkdir::{DirEntry, WalkDir};
use crate::settings::FilePattern;
use crate::checks::CheckCode;
use crate::settings::{FilePattern, PerFileIgnore};
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
@@ -25,9 +27,12 @@ fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
Ok((file_path, file_basename))
}
fn is_excluded(file_path: &str, file_basename: &str, exclude: &[FilePattern]) -> bool {
fn is_excluded<'a, T>(file_path: &str, file_basename: &str, exclude: T) -> bool
where
T: Iterator<Item = &'a FilePattern>,
{
for pattern in exclude {
match &pattern {
match pattern {
FilePattern::Simple(basename) => {
if *basename == file_basename {
return true;
@@ -45,7 +50,7 @@ fn is_excluded(file_path: &str, file_basename: &str, exclude: &[FilePattern]) ->
return true;
}
}
}
};
}
false
}
@@ -71,7 +76,6 @@ pub fn iter_python_files<'a>(
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
WalkDir::new(normalize_path(path))
.follow_links(true)
.into_iter()
.filter_entry(move |entry| {
if !has_exclude && !has_extend_exclude {
@@ -85,13 +89,13 @@ pub fn iter_python_files<'a>(
if has_exclude
&& (!exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, exclude)
&& is_excluded(file_path, file_basename, exclude.iter())
{
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if has_extend_exclude
&& (!extend_exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, extend_exclude)
&& is_excluded(file_path, file_basename, extend_exclude.iter())
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
@@ -107,11 +111,32 @@ pub fn iter_python_files<'a>(
})
.filter(|entry| {
entry.as_ref().map_or(true, |entry| {
(entry.depth() == 0 && !entry.file_type().is_dir()) || is_included(entry.path())
(entry.depth() == 0 || is_included(entry.path()))
&& !entry.file_type().is_dir()
&& !(entry.file_type().is_symlink() && entry.path().is_dir())
})
})
}
/// Create tree set with codes matching the pattern/code pairs.
pub fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [PerFileIgnore],
) -> Result<BTreeSet<&'a CheckCode>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()
.filter(|pattern_code_pair| {
is_excluded(
file_path,
file_basename,
[&pattern_code_pair.pattern].into_iter(),
)
})
.map(|pattern_code_pair| &pattern_code_pair.code)
.collect())
}
/// Convert any path to an absolute path (based on the current working directory).
pub fn normalize_path(path: &Path) -> PathBuf {
if let Ok(path) = path.absolutize() {
@@ -180,7 +205,7 @@ mod tests {
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
@@ -188,7 +213,7 @@ mod tests {
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
@@ -198,7 +223,7 @@ mod tests {
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
@@ -206,7 +231,7 @@ mod tests {
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
@@ -216,7 +241,7 @@ mod tests {
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
@@ -226,7 +251,7 @@ mod tests {
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
assert!(is_excluded(file_path, file_basename, exclude.iter()));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
@@ -236,7 +261,7 @@ mod tests {
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(file_path, file_basename, &exclude));
assert!(!is_excluded(file_path, file_basename, exclude.iter()));
Ok(())
}

View File

@@ -15,19 +15,30 @@ use crate::noqa::add_noqa;
use crate::settings::Settings;
use crate::{cache, fs, noqa};
/// Collect tokens up to and including the first error.
fn tokenize(contents: &str) -> Vec<LexResult> {
let mut tokens: Vec<LexResult> = vec![];
for tok in lexer::make_tokenizer(contents) {
let is_err = tok.is_err();
tokens.push(tok);
if is_err {
break;
}
}
tokens
}
fn check_path(
path: &Path,
contents: &str,
tokens: Vec<LexResult>,
noqa_line_for: &[usize],
settings: &Settings,
autofix: &fixer::Mode,
) -> Vec<Check> {
) -> Result<Vec<Check>> {
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Run the AST-based checks.
if settings
.select
@@ -50,9 +61,20 @@ fn check_path(
}
// Run the lines-based checks.
check_lines(&mut checks, contents, &noqa_line_for, settings, autofix);
check_lines(&mut checks, contents, noqa_line_for, settings, autofix);
checks
// Create path ignores.
if !checks.is_empty() && !settings.per_file_ignores.is_empty() {
let ignores = fs::ignores_from_path(path, &settings.per_file_ignores)?;
if !ignores.is_empty() {
return Ok(checks
.into_iter()
.filter(|check| !ignores.contains(check.kind.code()))
.collect());
}
}
Ok(checks)
}
pub fn lint_path(
@@ -73,10 +95,13 @@ pub fn lint_path(
let contents = fs::read_file(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = lexer::make_tokenizer(&contents).collect();
let tokens: Vec<LexResult> = tokenize(&contents);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let mut checks = check_path(path, &contents, tokens, settings, autofix);
let mut checks = check_path(path, &contents, tokens, &noqa_line_for, settings, autofix)?;
// Apply autofix.
if matches!(autofix, fixer::Mode::Apply) {
@@ -103,13 +128,20 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
let contents = fs::read_file(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = lexer::make_tokenizer(&contents).collect();
let tokens: Vec<LexResult> = tokenize(&contents);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Generate checks.
let checks = check_path(path, &contents, tokens, settings, &fixer::Mode::None);
let checks = check_path(
path,
&contents,
tokens,
&noqa_line_for,
settings,
&fixer::Mode::None,
)?;
add_noqa(&checks, &contents, &noqa_line_for, path)
}
@@ -119,14 +151,15 @@ mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer;
use regex::Regex;
use rustpython_parser::lexer::LexResult;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::fs;
use crate::linter;
use crate::linter::tokenize;
use crate::settings;
use crate::{fs, noqa};
fn check_path(
path: &Path,
@@ -134,10 +167,9 @@ mod tests {
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = lexer::make_tokenizer(&contents).collect();
Ok(linter::check_path(
path, &contents, tokens, settings, autofix,
))
let tokens: Vec<LexResult> = tokenize(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(path, &contents, tokens, &noqa_line_for, settings, autofix)
}
#[test]
@@ -596,6 +628,21 @@ mod tests {
Ok(())
}
#[test]
fn f841_dummy_variable_rgx() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings {
dummy_variable_rgx: Regex::new(r"^z$").unwrap(),
..settings::Settings::for_rule(CheckCode::F841)
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn f901() -> Result<()> {
let mut checks = check_path(
@@ -667,4 +714,52 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn e999() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E999.py"),
&settings::Settings::for_rule(CheckCode::E999),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a001() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A001.py"),
&settings::Settings::for_rule(CheckCode::A001),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a002() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A002.py"),
&settings::Settings::for_rule(CheckCode::A002),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn a003() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/A003.py"),
&settings::Settings::for_rule(CheckCode::A003),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -7,84 +7,91 @@ use std::sync::mpsc::channel;
use std::time::Instant;
use anyhow::Result;
use clap::{Parser, ValueHint};
use clap::{command, Parser};
use colored::Colorize;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use regex::Regex;
use walkdir::DirEntry;
use ::ruff::cache;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::add_noqa_to_path;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::pyproject;
use ::ruff::settings::{FilePattern, Settings};
use ::ruff::pyproject::{self, StrCheckCodePair};
use ::ruff::settings::{FilePattern, PerFileIgnore, Settings};
use ::ruff::tell_user;
use ruff::linter::add_noqa_to_path;
use ruff::settings::CurrentSettings;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Parser)]
#[clap(name = format!("{CARGO_PKG_NAME} (v{CARGO_PKG_VERSION})"))]
#[clap(about = "An extremely fast Python linter.", long_about = None)]
#[clap(version)]
#[command(author, about = "ruff: An extremely fast Python linter.")]
#[command(version)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::AnyPath, required = true)]
#[arg(required = true)]
files: Vec<PathBuf>,
/// Enable verbose logging.
#[clap(short, long, action)]
#[arg(short, long)]
verbose: bool,
/// Disable all logging (but still exit with status code "1" upon detecting errors).
#[clap(short, long, action)]
#[arg(short, long)]
quiet: bool,
/// Exit with status code "0", even upon detecting errors.
#[clap(short, long, action)]
#[arg(short, long)]
exit_zero: bool,
/// Run in watch mode by re-running whenever files change.
#[clap(short, long, action)]
#[arg(short, long)]
watch: bool,
/// Attempt to automatically fix lint errors.
#[clap(short, long, action)]
#[arg(short, long)]
fix: bool,
/// Disable cache reads.
#[clap(short, long, action)]
#[arg(short, long)]
no_cache: bool,
/// List of error codes to enable.
#[clap(long, multiple = true)]
#[arg(long, value_delimiter = ',')]
select: Vec<CheckCode>,
/// Like --select, but adds additional error codes on top of the selected ones.
#[clap(long, multiple = true)]
#[arg(long, value_delimiter = ',')]
extend_select: Vec<CheckCode>,
/// List of error codes to ignore.
#[clap(long, multiple = true)]
#[arg(long, value_delimiter = ',')]
ignore: Vec<CheckCode>,
/// Like --ignore, but adds additional error codes on top of the ignored ones.
#[clap(long, multiple = true)]
#[arg(long, value_delimiter = ',')]
extend_ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[clap(long, multiple = true)]
#[arg(long, value_delimiter = ',')]
exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[clap(long, multiple = true)]
#[arg(long, value_delimiter = ',')]
extend_exclude: Vec<String>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
per_file_ignores: Vec<StrCheckCodePair>,
/// Output serialization format for error messages.
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
/// See the files ruff will be run against with the current settings.
#[clap(long, action)]
#[arg(long)]
show_files: bool,
/// See ruff's settings.
#[clap(long, action)]
#[arg(long)]
show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[clap(long, action)]
#[arg(long)]
add_noqa: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
dummy_variable_rgx: Option<Regex>,
}
#[cfg(feature = "update-informer")]
@@ -111,8 +118,8 @@ fn check_for_updates() {
}
}
fn show_settings(settings: &Settings) {
println!("{:#?}", settings);
fn show_settings(settings: Settings) {
println!("{:#?}", CurrentSettings::from_settings(settings));
}
fn show_files(files: &[PathBuf], settings: &Settings) {
@@ -244,14 +251,22 @@ fn inner_main() -> Result<ExitCode> {
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
let per_file_ignores: Vec<PerFileIgnore> = cli
.per_file_ignores
.into_iter()
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect();
let mut settings = Settings::from_pyproject(pyproject, project_root);
let mut settings = Settings::from_pyproject(pyproject, project_root)?;
if !exclude.is_empty() {
settings.exclude = exclude;
}
if !extend_exclude.is_empty() {
settings.extend_exclude = extend_exclude;
}
if !per_file_ignores.is_empty() {
settings.per_file_ignores = per_file_ignores;
}
if !cli.select.is_empty() {
settings.clear();
settings.select(cli.select);
@@ -265,34 +280,37 @@ fn inner_main() -> Result<ExitCode> {
if !cli.extend_ignore.is_empty() {
settings.ignore(&cli.extend_ignore);
}
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
settings.dummy_variable_rgx = dummy_variable_rgx;
}
if cli.show_settings && cli.show_files {
println!("Error: specify --show-settings or show-files (not both).");
eprintln!("Error: specify --show-settings or show-files (not both).");
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(&settings);
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
show_files(&cli.files, &settings);
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {
show_settings(settings);
return Ok(ExitCode::SUCCESS);
}
cache::init()?;
let mut printer = Printer::new(cli.format, cli.verbose);
if cli.watch {
if cli.fix {
println!("Warning: --fix is not enabled in watch mode.");
eprintln!("Warning: --fix is not enabled in watch mode.");
}
if cli.add_noqa {
println!("Warning: --no-qa is not enabled in watch mode.");
eprintln!("Warning: --no-qa is not enabled in watch mode.");
}
if cli.format != SerializationFormat::Text {
println!("Warning: --format 'text' is used in watch mode.");
eprintln!("Warning: --format 'text' is used in watch mode.");
}
// Perform an initial run instantly.
@@ -354,6 +372,9 @@ fn inner_main() -> Result<ExitCode> {
fn main() -> ExitCode {
match inner_main() {
Ok(code) => code,
Err(_) => ExitCode::FAILURE,
Err(err) => {
eprintln!("{} {:?}", "error".red().bold(), err);
ExitCode::FAILURE
}
}
}

View File

@@ -1,13 +1,14 @@
use std::cmp::{max, min};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use crate::checks::{Check, CheckCode};
use anyhow::Result;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::lexer::{LexResult, Tok};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::Path;
use crate::checks::{Check, CheckCode};
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)(?P<noqa>\s*# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
@@ -160,12 +161,12 @@ pub fn add_noqa(
#[cfg(test)]
mod tests {
use crate::checks::{Check, CheckKind};
use anyhow::Result;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::checks::{Check, CheckKind};
use crate::noqa::{add_noqa_inner, extract_noqa_line_for};
#[test]

View File

@@ -1,7 +1,10 @@
use anyhow::Result;
use clap::ValueEnum;
use colored::Colorize;
use rustpython_parser::ast::Location;
use serde::Serialize;
use crate::checks::{CheckCode, CheckKind};
use crate::message::Message;
use crate::tell_user;
@@ -11,6 +14,16 @@ pub enum SerializationFormat {
Json,
}
#[derive(Serialize)]
struct ExpandedMessage<'a> {
kind: &'a CheckKind,
code: &'a CheckCode,
message: String,
fixed: bool,
location: Location,
filename: &'a String,
}
pub struct Printer {
format: SerializationFormat,
verbose: bool,
@@ -31,7 +44,22 @@ impl Printer {
match self.format {
SerializationFormat::Json => {
println!("{}", serde_json::to_string_pretty(&messages)?)
println!(
"{}",
serde_json::to_string_pretty(
&messages
.iter()
.map(|m| ExpandedMessage {
kind: &m.kind,
code: m.kind.code(),
message: m.kind.body(),
fixed: m.fixed,
location: m.location,
filename: &m.filename,
})
.collect::<Vec<_>>()
)?
)
}
SerializationFormat::Text => {
if !fixed.is_empty() {

View File

@@ -1,30 +1,25 @@
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::Result;
use anyhow::{anyhow, Result};
use common_path::common_path_all;
use path_absolutize::Absolutize;
use serde::Deserialize;
use serde::de;
use serde::{Deserialize, Deserializer};
use crate::checks::CheckCode;
use crate::fs;
pub fn load_config(pyproject: &Option<PathBuf>) -> Config {
pub fn load_config(pyproject: &Option<PathBuf>) -> Result<Config> {
match pyproject {
Some(pyproject) => match parse_pyproject_toml(pyproject) {
Ok(pyproject) => pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default(),
Err(e) => {
println!("Failed to load pyproject.toml: {:?}", e);
println!("Falling back to default configuration...");
Default::default()
}
},
Some(pyproject) => Ok(parse_pyproject_toml(pyproject)?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default()),
None => {
println!("No pyproject.toml found.");
println!("Falling back to default configuration...");
Default::default()
eprintln!("No pyproject.toml found.");
eprintln!("Falling back to default configuration...");
Ok(Default::default())
}
}
}
@@ -37,6 +32,50 @@ pub struct Config {
pub extend_exclude: Option<Vec<String>>,
pub select: Option<Vec<CheckCode>>,
pub ignore: Option<Vec<CheckCode>>,
pub per_file_ignores: Option<Vec<StrCheckCodePair>>,
pub dummy_variable_rgx: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StrCheckCodePair {
pub pattern: String,
pub code: CheckCode,
}
impl StrCheckCodePair {
const EXPECTED_PATTERN: &'static str = "<FilePattern>:<CheckCode> pattern";
}
impl<'de> Deserialize<'de> for StrCheckCodePair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let str_result = String::deserialize(deserializer)?;
Self::from_str(str_result.as_str()).map_err(|_| {
de::Error::invalid_value(
de::Unexpected::Str(str_result.as_str()),
&Self::EXPECTED_PATTERN,
)
})
}
}
impl FromStr for StrCheckCodePair {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let (pattern_str, code_string) = {
let tokens = string.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
}
(tokens[0], tokens[1])
};
let code = CheckCode::from_str(code_string)?;
let pattern = pattern_str.into();
Ok(Self { pattern, code })
}
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
@@ -102,9 +141,11 @@ pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
mod tests {
use std::env::current_dir;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::Result;
use super::StrCheckCodePair;
use crate::checks::CheckCode;
use crate::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
@@ -137,6 +178,8 @@ mod tests {
extend_exclude: None,
select: None,
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -157,6 +200,8 @@ line-length = 79
extend_exclude: None,
select: None,
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -177,6 +222,8 @@ exclude = ["foo.py"]
extend_exclude: None,
select: None,
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -197,6 +244,8 @@ select = ["E501"]
extend_exclude: None,
select: Some(vec![CheckCode::E501]),
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -217,6 +266,8 @@ ignore = ["E501"]
extend_exclude: None,
select: None,
ignore: Some(vec![CheckCode::E501]),
per_file_ignores: None,
dummy_variable_rgx: None,
})
})
);
@@ -281,9 +332,29 @@ other-attribute = 1
]),
select: None,
ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
}
);
Ok(())
}
#[test]
fn str_check_code_pair_strings() {
let result = StrCheckCodePair::from_str("foo:E501");
assert!(result.is_ok());
let result = StrCheckCodePair::from_str("E501:foo");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("E501");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("foo");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("foo:E501:E402");
assert!(result.is_err());
let result = StrCheckCodePair::from_str("**/bar:E501");
assert!(result.is_ok());
let result = StrCheckCodePair::from_str("bar:E502");
assert!(result.is_err());
}
}

View File

@@ -2,14 +2,16 @@ use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use glob::Pattern;
use once_cell::sync::Lazy;
use regex::Regex;
use crate::checks::{CheckCode, DEFAULT_CHECK_CODES};
use crate::fs;
use crate::pyproject::load_config;
use crate::pyproject::{load_config, StrCheckCodePair};
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Hash)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
@@ -34,6 +36,20 @@ impl FilePattern {
}
}
#[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore {
pub pattern: FilePattern,
pub code: CheckCode,
}
impl PerFileIgnore {
pub fn new(user_in: StrCheckCodePair, project_root: &Option<PathBuf>) -> Self {
let pattern = FilePattern::from_user(user_in.pattern.as_str(), project_root);
let code = user_in.code;
Self { pattern, code }
}
}
#[derive(Debug)]
pub struct Settings {
pub pyproject: Option<PathBuf>,
@@ -42,6 +58,8 @@ pub struct Settings {
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub select: BTreeSet<CheckCode>,
pub per_file_ignores: Vec<PerFileIgnore>,
pub dummy_variable_rgx: Regex,
}
impl Settings {
@@ -53,6 +71,8 @@ impl Settings {
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([check_code]),
per_file_ignores: vec![],
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
}
}
@@ -64,6 +84,8 @@ impl Settings {
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from_iter(check_codes),
per_file_ignores: vec![],
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
}
}
}
@@ -71,9 +93,13 @@ impl Settings {
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.line_length.hash(state);
self.dummy_variable_rgx.as_str().hash(state);
for value in self.select.iter() {
value.hash(state);
}
for value in self.per_file_ignores.iter() {
value.hash(state);
}
}
}
@@ -101,9 +127,15 @@ static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
]
});
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl Settings {
pub fn from_pyproject(pyproject: Option<PathBuf>, project_root: Option<PathBuf>) -> Self {
let config = load_config(&pyproject);
pub fn from_pyproject(
pyproject: Option<PathBuf>,
project_root: Option<PathBuf>,
) -> Result<Self> {
let config = load_config(&pyproject)?;
let mut settings = Settings {
line_length: config.line_length.unwrap_or(88),
exclude: config
@@ -129,13 +161,27 @@ impl Settings {
} else {
BTreeSet::from_iter(DEFAULT_CHECK_CODES)
},
per_file_ignores: config
.per_file_ignores
.map(|ignore_strings| {
ignore_strings
.into_iter()
.map(|pair| PerFileIgnore::new(pair, &project_root))
.collect()
})
.unwrap_or_default(),
dummy_variable_rgx: match config.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
pyproject,
project_root,
};
if let Some(ignore) = &config.ignore {
settings.ignore(ignore);
}
settings
Ok(settings)
}
pub fn clear(&mut self) {
@@ -154,3 +200,62 @@ impl Settings {
}
}
}
/// Struct to render user-facing exclusion patterns.
#[derive(Debug)]
#[allow(dead_code)]
pub struct Exclusion {
basename: Option<String>,
absolute: Option<String>,
}
impl Exclusion {
pub fn from_file_pattern(file_pattern: FilePattern) -> Self {
match file_pattern {
FilePattern::Simple(basename) => Exclusion {
basename: Some(basename.to_string()),
absolute: None,
},
FilePattern::Complex(absolute, basename) => Exclusion {
basename: basename.map(|pattern| pattern.to_string()),
absolute: Some(absolute.to_string()),
},
}
}
}
/// Struct to render user-facing Settings.
#[derive(Debug)]
pub struct CurrentSettings {
pub pyproject: Option<PathBuf>,
pub project_root: Option<PathBuf>,
pub line_length: usize,
pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>,
pub select: BTreeSet<CheckCode>,
pub per_file_ignores: Vec<PerFileIgnore>,
pub dummy_variable_rgx: Regex,
}
impl CurrentSettings {
pub fn from_settings(settings: Settings) -> Self {
Self {
pyproject: settings.pyproject,
project_root: settings.project_root,
line_length: settings.line_length,
exclude: settings
.exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_exclude: settings
.extend_exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
select: settings.select,
per_file_ignores: settings.per_file_ignores,
dummy_variable_rgx: settings.dummy_variable_rgx,
}
}
}

View File

@@ -0,0 +1,112 @@
---
source: src/linter.rs
expression: checks
---
- kind:
BuiltinVariableShadowing: sum
location:
row: 1
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: int
location:
row: 2
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: print
location:
row: 4
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: copyright
location:
row: 5
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: complex
location:
row: 6
column: 2
fix: ~
- kind:
BuiltinVariableShadowing: float
location:
row: 7
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: object
location:
row: 7
column: 9
fix: ~
- kind:
BuiltinVariableShadowing: min
location:
row: 8
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: max
location:
row: 8
column: 6
fix: ~
- kind:
BuiltinVariableShadowing: bytes
location:
row: 10
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: slice
location:
row: 13
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: ValueError
location:
row: 18
column: 1
fix: ~
- kind:
BuiltinVariableShadowing: memoryview
location:
row: 21
column: 5
fix: ~
- kind:
BuiltinVariableShadowing: bytearray
location:
row: 21
column: 18
fix: ~
- kind:
BuiltinVariableShadowing: str
location:
row: 24
column: 22
fix: ~
- kind:
BuiltinVariableShadowing: all
location:
row: 24
column: 45
fix: ~
- kind:
BuiltinVariableShadowing: any
location:
row: 24
column: 50
fix: ~
- kind:
BuiltinVariableShadowing: sum
location:
row: 27
column: 8
fix: ~

View File

@@ -0,0 +1,46 @@
---
source: src/linter.rs
expression: checks
---
- kind:
BuiltinArgumentShadowing: str
location:
row: 1
column: 11
fix: ~
- kind:
BuiltinArgumentShadowing: type
location:
row: 1
column: 19
fix: ~
- kind:
BuiltinArgumentShadowing: complex
location:
row: 1
column: 26
fix: ~
- kind:
BuiltinArgumentShadowing: Exception
location:
row: 1
column: 35
fix: ~
- kind:
BuiltinArgumentShadowing: getattr
location:
row: 1
column: 48
fix: ~
- kind:
BuiltinArgumentShadowing: bytes
location:
row: 5
column: 17
fix: ~
- kind:
BuiltinArgumentShadowing: float
location:
row: 9
column: 16
fix: ~

View File

@@ -0,0 +1,18 @@
---
source: src/linter.rs
assertion_line: 762
expression: checks
---
- kind:
BuiltinAttributeShadowing: ImportError
location:
row: 2
column: 5
fix: ~
- kind:
BuiltinAttributeShadowing: str
location:
row: 7
column: 5
fix: ~

View File

@@ -0,0 +1,11 @@
---
source: src/linter.rs
expression: checks
---
- kind:
SyntaxError: Got unexpected EOF
location:
row: 2
column: 1
fix: ~

View File

@@ -0,0 +1,47 @@
---
source: src/linter.rs
expression: checks
---
- kind:
UnusedVariable: e
location:
row: 3
column: 1
fix: ~
- kind:
UnusedVariable: foo
location:
row: 20
column: 5
fix: ~
- kind:
UnusedVariable: a
location:
row: 21
column: 6
fix: ~
- kind:
UnusedVariable: b
location:
row: 21
column: 9
fix: ~
- kind:
UnusedVariable: _
location:
row: 35
column: 5
fix: ~
- kind:
UnusedVariable: __
location:
row: 36
column: 5
fix: ~
- kind:
UnusedVariable: _discarded
location:
row: 37
column: 5
fix: ~