Compare commits

..

26 Commits

Author SHA1 Message Date
Charlie Marsh
27cc7e236c Use a separate repo for pre-commit (#229) 2022-09-19 21:06:39 -06:00
Charlie Marsh
fa0954fe47 Treat relative excludes as relative to project root (#228) 2022-09-19 20:45:02 -06:00
Charlie Marsh
a0b50d7ebc Use absolute paths for exclusion matching (#213) 2022-09-19 20:32:31 -06:00
Charlie Marsh
afe7a04211 Ignore F841 violations when locals() is in scope (#226) 2022-09-19 20:13:55 -06:00
Charlie Marsh
14806c62ca Reduce number of sites for new check definitions (#227) 2022-09-19 20:13:46 -06:00
Suguru Yamamoto
0d0c8730fa fix: Use UTF-32 char count for line length (#223) (#224) 2022-09-18 10:45:41 -06:00
Harutaka Kawamura
cf6a23b83c Add --version flag (#222) 2022-09-18 09:15:15 -06:00
Anders Kaseorg
9e0daac561 Enable F404 by default (#219) 2022-09-18 09:13:23 -06:00
Anders Kaseorg
f2fd7335ce Use a platform-appropriate location for user configuration (#215) 2022-09-17 13:29:17 -06:00
Anders Kaseorg
b8f878df5e Find user configuration even if there’s no project directory (#216) 2022-09-16 21:47:15 -06:00
Anders Kaseorg
9bdb922c75 Detect multi-target assignment as unpacking if *any* target is unpacking (#217) 2022-09-16 21:45:44 -06:00
Anders Kaseorg
edecc1bba6 Fix find_project_root with relative paths (#214) 2022-09-16 18:04:35 -06:00
Charlie Marsh
8e903153f6 Update README to include more badges 2022-09-16 12:18:03 -06:00
Charlie Marsh
3937885f37 Bump version to 0.0.40 2022-09-16 04:57:21 -04:00
Charlie Marsh
24de97d951 Create cache directory prior to writing .gitignore 2022-09-16 04:56:58 -04:00
Charlie Marsh
06e5b3e457 Bump version to 0.0.39 2022-09-15 21:41:14 -04:00
Charlie Marsh
68a0e6dc19 Remove erroneous test dir 2022-09-15 21:41:00 -04:00
Charlie Marsh
9d4a4478f7 Improve exclusion syntax to match exact files (#209) 2022-09-15 21:40:49 -04:00
Charlie Marsh
6bbf3f46c4 Add .gitignore to .ruff_cache (#208) 2022-09-15 20:40:06 -04:00
Charlie Marsh
4ac4e8c991 Exclude .ruff_cache by default (#207) 2022-09-15 20:39:39 -04:00
Dmitry Dygalo
0091a3ae5f chore: Do not read the same file twice (#206) 2022-09-15 16:05:29 -04:00
Patrick Haller
17b3109a8b Update docs with --format flag (#205) 2022-09-15 16:04:07 -04:00
Charlie Marsh
71520213c1 Allow __path__ in __init__.py (#201) 2022-09-15 09:44:03 -04:00
Charlie Marsh
f24e7a0052 Add trailing period to help message 2022-09-15 09:43:51 -04:00
Patrick Haller
507e9f7ec3 Fix: Structured output Issue Fix (#186) 2022-09-15 09:43:10 -04:00
Charlie Marsh
592c53c8bf Use binding location when reporting F821 errors (#200) 2022-09-14 22:51:07 -04:00
29 changed files with 828 additions and 463 deletions

View File

@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.38
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.40
hooks:
- id: lint

View File

@@ -1,7 +0,0 @@
- id: lint
name: ruff lint
description: Run ruff to lint Python files.
entry: ruff
language: python
types_or: [python]
pass_filenames: true

21
Cargo.lock generated
View File

@@ -1432,6 +1432,24 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "path-absolutize"
version = "3.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3de4b40bd9736640f14c438304c09538159802388febb02c8abaae0846c1f13"
dependencies = [
"path-dedot",
]
[[package]]
name = "path-dedot"
version = "3.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d611d5291372b3738a34ebf0d1f849e58b1dcc1101032f76a346eaa1f8ddbb5b"
dependencies = [
"once_cell",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@@ -1783,7 +1801,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.38"
version = "0.0.40"
dependencies = [
"anyhow",
"bincode",
@@ -1802,6 +1820,7 @@ dependencies = [
"log",
"notify",
"once_cell",
"path-absolutize",
"rayon",
"regex",
"rustpython-parser",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.38"
version = "0.0.40"
edition = "2021"
[lib]
@@ -23,6 +23,7 @@ itertools = "0.10.3"
log = { version = "0.4.17" }
notify = { version = "4.0.17" }
once_cell = { version = "1.13.1" }
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "7d21c6923a506e79cc041708d83cef925efd33f4" }

107
README.md
View File

@@ -1,7 +1,9 @@
# ruff
[![image](https://img.shields.io/pypi/v/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/charliermarsh/ruff/workflows/CI/badge.svg)](https://github.com/charliermarsh/ruff/actions)
[![PyPI version](https://badge.fury.io/py/ruff.svg)](https://badge.fury.io/py/ruff)
An extremely fast Python linter, written in Rust.
@@ -52,14 +54,14 @@ You can run ruff in `--watch` mode to automatically re-run on-change:
ruff path/to/code/ --watch
```
ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on system):
ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff
rev: v0.0.38
hooks:
- id: lint
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.40
hooks:
- id: lint
```
## Configuration
@@ -86,7 +88,7 @@ ruff path/to/code/ --select F401 F403
See `ruff --help` for more:
```shell
ruff (v0.0.38)
ruff (v0.0.40)
An extremely fast Python linter.
USAGE:
@@ -96,19 +98,44 @@ ARGS:
<FILES>...
OPTIONS:
-e, --exit-zero Exit with status code "0", even upon detecting errors
--exclude <EXCLUDE>... List of file and/or directory patterns to exclude from checks
-f, --fix Attempt to automatically fix lint errors
-h, --help Print help information
--ignore <IGNORE>... List of error codes to ignore
-n, --no-cache Disable cache reads
-q, --quiet Disable all logging (but still exit with status code "1" upon
detecting errors)
--select <SELECT>... List of error codes to enable
-v, --verbose Enable verbose logging
-w, --watch Run in watch mode by re-running whenever files change
-e, --exit-zero
Exit with status code "0", even upon detecting errors
--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
-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
--ignore <IGNORE>...
List of error codes to ignore
-n, --no-cache
Disable cache reads
-q, --quiet
Disable all logging (but still exit with status code "1" upon detecting errors)
--select <SELECT>...
List of error codes to enable
-v, --verbose
Enable verbose logging
-V, --version
Print version information
-w, --watch
Run in watch mode by re-running whenever files change
```
Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching
`foo_*.py` ).
- Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py`
(to exclude any Python files in `directory`). Note that these paths are relative to the
project root (e.g., the directory containing your `pyproject.toml`).
### Compatibility with Black
ruff is intended to be compatible with [Black](https://github.com/psf/black), and should be
@@ -221,28 +248,28 @@ Add this `pyproject.toml` to the CPython directory:
[tool.ruff]
line-length = 88
exclude = [
"Lib/lib2to3/tests/data/bom.py",
"Lib/lib2to3/tests/data/crlf.py",
"Lib/lib2to3/tests/data/different_encoding.py",
"Lib/lib2to3/tests/data/false_encoding.py",
"Lib/lib2to3/tests/data/py2_test_grammar.py",
"Lib/test/bad_coding2.py",
"Lib/test/badsyntax_3131.py",
"Lib/test/badsyntax_pep3120.py",
"Lib/test/encoded_modules/module_iso_8859_1.py",
"Lib/test/encoded_modules/module_koi8_r.py",
"Lib/test/test_fstring.py",
"Lib/test/test_grammar.py",
"Lib/test/test_importlib/test_util.py",
"Lib/test/test_named_expressions.py",
"Lib/test/test_patma.py",
"Lib/test/test_source_encoding.py",
"Tools/c-analyzer/c_parser/parser/_delim.py",
"Tools/i18n/pygettext.py",
"Tools/test2to3/maintest.py",
"Tools/test2to3/setup.py",
"Tools/test2to3/test/test_foo.py",
"Tools/test2to3/test2to3/hello.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/bom.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/crlf.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/different_encoding.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/false_encoding.py",
"./resources/test/cpython/Lib/lib2to3/tests/data/py2_test_grammar.py",
"./resources/test/cpython/Lib/test/bad_coding2.py",
"./resources/test/cpython/Lib/test/badsyntax_3131.py",
"./resources/test/cpython/Lib/test/badsyntax_pep3120.py",
"./resources/test/cpython/Lib/test/encoded_modules/module_iso_8859_1.py",
"./resources/test/cpython/Lib/test/encoded_modules/module_koi8_r.py",
"./resources/test/cpython/Lib/test/test_fstring.py",
"./resources/test/cpython/Lib/test/test_grammar.py",
"./resources/test/cpython/Lib/test/test_importlib/test_util.py",
"./resources/test/cpython/Lib/test/test_named_expressions.py",
"./resources/test/cpython/Lib/test/test_patma.py",
"./resources/test/cpython/Lib/test/test_source_encoding.py",
"./resources/test/cpython/Tools/c-analyzer/c_parser/parser/_delim.py",
"./resources/test/cpython/Tools/i18n/pygettext.py",
"./resources/test/cpython/Tools/test2to3/maintest.py",
"./resources/test/cpython/Tools/test2to3/setup.py",
"./resources/test/cpython/Tools/test2to3/test/test_foo.py",
"./resources/test/cpython/Tools/test2to3/test2to3/hello.py",
]
```

View File

@@ -1,56 +1,14 @@
/// Generate a Markdown-compatible table of supported lint rules.
use ruff::checks::{CheckKind, RejectedCmpop};
use ruff::checks::{CheckCode, ALL_CHECK_CODES};
fn main() {
let mut check_kinds: Vec<CheckKind> = vec![
CheckKind::AmbiguousClassName("...".to_string()),
CheckKind::AmbiguousFunctionName("...".to_string()),
CheckKind::AmbiguousVariableName("...".to_string()),
CheckKind::AssertTuple,
CheckKind::BreakOutsideLoop,
CheckKind::ContinueOutsideLoop,
CheckKind::DefaultExceptNotLast,
CheckKind::DoNotAssignLambda,
CheckKind::DoNotUseBareExcept,
CheckKind::DuplicateArgumentName,
CheckKind::FStringMissingPlaceholders,
CheckKind::ForwardAnnotationSyntaxError("...".to_string()),
CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckKind::IOError("...".to_string()),
CheckKind::IfTuple,
CheckKind::ImportStarNotPermitted("...".to_string()),
CheckKind::ImportStarUsage("...".to_string()),
CheckKind::InvalidPrintSyntax,
CheckKind::IsLiteral,
CheckKind::LateFutureImport,
CheckKind::LineTooLong(89, 88),
CheckKind::ModuleImportNotAtTopOfFile,
CheckKind::MultiValueRepeatedKeyLiteral,
CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckKind::NoAssertEquals,
CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckKind::NotInTest,
CheckKind::NotIsTest,
CheckKind::RaiseNotImplemented,
CheckKind::ReturnOutsideFunction,
CheckKind::SyntaxError("...".to_string()),
CheckKind::TooManyExpressionsInStarredAssignment,
CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckKind::TwoStarredExpressions,
CheckKind::TypeComparison,
CheckKind::UndefinedExport("...".to_string()),
CheckKind::UndefinedLocal("...".to_string()),
CheckKind::UndefinedName("...".to_string()),
CheckKind::UnusedImport("...".to_string()),
CheckKind::UnusedVariable("...".to_string()),
CheckKind::UselessObjectInheritance("...".to_string()),
CheckKind::YieldOutsideFunction,
];
check_kinds.sort_by_key(|check_kind| check_kind.code());
let mut check_codes: Vec<CheckCode> = ALL_CHECK_CODES.to_vec();
check_codes.sort();
println!("| Code | Name | Message |");
println!("| ---- | ----- | ------- |");
for check_kind in check_kinds {
for check_code in check_codes {
let check_kind = check_code.kind();
println!(
"| {} | {} | {} |",
check_kind.code().as_str(),

View File

@@ -4,3 +4,6 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
"""
_ = "---------------------------------------------------------------------------AAAAAAA"
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜"

View File

@@ -77,3 +77,8 @@ class Ticket:
def set_status(self, status: Status):
self.status = status
def update_tomato():
print(TOMATO)
TOMATO = "cherry tomato"

View File

@@ -22,3 +22,10 @@ def g():
bar = (1, 2)
(c, d) = bar
(x, y) = baz = bar
def h():
locals()
x = 1

View File

@@ -0,0 +1,3 @@
print(__path__)
__all__ = ["a", "b", "c"]

View File

@@ -0,0 +1,9 @@
a = "abc"
b = f"ghi{'jkl'}"
c = f"def"
d = f"def" + "ghi"
e = (
f"def" +
"ghi"
)

View File

@@ -1,47 +1,7 @@
[tool.ruff]
line-length = 88
extend-exclude = ["excluded.py", "migrations"]
select = [
"E402",
"E501",
"E711",
"E712",
"E713",
"E714",
"E721",
"E722",
"E731",
"E741",
"E742",
"E743",
"E902",
"E999",
"F401",
"F403",
"F404",
"F406",
"F407",
"F541",
"F601",
"F602",
"F621",
"F622",
"F631",
"F632",
"F633",
"F634",
"F701",
"F702",
"F704",
"F706",
"F707",
"F722",
"F821",
"F822",
"F823",
"F831",
"F841",
"F901",
"R001",
"R002",
extend-exclude = [
"excluded.py",
"migrations",
"directory/also_excluded.py",
]

View File

@@ -7,7 +7,7 @@ use rustpython_parser::ast::{
};
use crate::ast::operations::SourceCodeLocator;
use crate::ast::types::{Binding, BindingKind, Scope};
use crate::ast::types::{Binding, BindingKind, FunctionScope, Scope, ScopeKind};
use crate::autofix::{fixer, fixes};
use crate::checks::{Check, CheckKind, Fix, RejectedCmpop};
@@ -67,6 +67,13 @@ pub fn check_not_tests(
pub fn check_unused_variables(scope: &Scope) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
if matches!(
scope.kind,
ScopeKind::Function(FunctionScope { uses_locals: true })
) {
return checks;
}
for (name, binding) in scope.values.iter() {
// TODO(charlie): Ignore if using `locals`.
if binding.used.is_none()

View File

@@ -101,11 +101,13 @@ pub fn in_nested_block(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
/// Check if a node represents an unpacking assignment.
pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
if let StmtKind::Assign { targets, value, .. } = &stmt.node {
for child in targets {
match &child.node {
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. } => {}
_ => return false,
}
if !targets.iter().any(|child| {
matches!(
child.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
}) {
return false;
}
match &value.node {
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. } => return false,

View File

@@ -8,10 +8,15 @@ fn id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[derive(Clone, Debug, Default)]
pub struct FunctionScope {
pub uses_locals: bool,
}
#[derive(Clone, Debug)]
pub enum ScopeKind {
Class,
Function,
Function(FunctionScope),
Generator,
Module,
}
@@ -53,5 +58,7 @@ pub enum BindingKind {
pub struct Binding {
pub kind: BindingKind,
pub location: Location,
pub used: Option<usize>,
/// Tuple of (scope index, location) indicating the scope and location at which the binding was
/// last used.
pub used: Option<(usize, Location)>,
}

View File

@@ -1,11 +1,14 @@
use std::collections::hash_map::DefaultHasher;
use std::fs::Metadata;
use std::fs::{create_dir_all, File, Metadata};
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::path::Path;
use anyhow::Result;
use cacache::Error::EntryNotFound;
use filetime::FileTime;
use log::error;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use crate::autofix::fixer;
@@ -77,12 +80,22 @@ fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String
autofix.hash(&mut hasher);
format!(
"{}@{}@{}",
path.canonicalize().unwrap().to_string_lossy(),
path.absolutize().unwrap().to_string_lossy(),
VERSION,
hasher.finish()
)
}
pub fn init() -> Result<()> {
let gitignore_path = Path::new(cache_dir()).join(".gitignore");
if gitignore_path.exists() {
return Ok(());
}
create_dir_all(cache_dir())?;
let mut file = File::create(gitignore_path)?;
file.write_all(b"*").map_err(|e| e.into())
}
pub fn get(
path: &Path,
metadata: &Metadata,

View File

@@ -9,7 +9,7 @@ use rustpython_parser::parser;
use crate::ast::operations::{extract_all_names, SourceCodeLocator};
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{Binding, BindingKind, Scope, ScopeKind};
use crate::ast::types::{Binding, BindingKind, FunctionScope, Scope, ScopeKind};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{checks, operations, visitor};
use crate::autofix::fixer;
@@ -26,7 +26,7 @@ struct Checker<'a> {
locator: SourceCodeLocator<'a>,
settings: &'a Settings,
autofix: &'a fixer::Mode,
path: &'a str,
path: &'a Path,
// Computed checks.
checks: Vec<Check>,
// Retain all scopes and parent nodes, along with a stack of indexes to track which are active
@@ -55,7 +55,7 @@ impl<'a> Checker<'a> {
pub fn new(
settings: &'a Settings,
autofix: &'a fixer::Mode,
path: &'a str,
path: &'a Path,
content: &'a str,
) -> Checker<'a> {
Checker {
@@ -184,7 +184,7 @@ where
name.to_string(),
Binding {
kind: BindingKind::Assignment,
used: Some(global_scope_id),
used: Some((global_scope_id, stmt.location)),
location: stmt.location,
},
);
@@ -416,13 +416,14 @@ where
name,
Binding {
kind: BindingKind::FutureImportation,
used: Some(
used: Some((
self.scopes[*(self
.scope_stack
.last()
.expect("No current scope found."))]
.id,
),
stmt.location,
)),
location: stmt.location,
},
);
@@ -645,6 +646,33 @@ where
self.checks.push(check)
}
}
if let ExprKind::Name { id, ctx } = &func.node {
if id == "locals" && matches!(ctx, ExprContext::Load) {
let scope = &mut self.scopes[*(self
.scope_stack
.last_mut()
.expect("No current scope found."))];
if matches!(
scope.kind,
ScopeKind::Function(FunctionScope { uses_locals: false })
) {
scope.kind = ScopeKind::Function(FunctionScope { uses_locals: true });
}
}
}
//
// if id == "locals" {
// let scope = &self.scopes
// [*(self.scope_stack.last().expect("No current scope found."))];
// if matches!(scope.kind, ScopeKind::Function(_)) {
// let parent =
// self.parents[*(self.parent_stack.last().expect("No parent found."))];
// if matches!(parent.node, StmtKind::Call)
// }
//
// }
}
ExprKind::Dict { keys, .. } => {
let check_repeated_literals = self.settings.select.contains(&CheckCode::F601);
@@ -1108,7 +1136,7 @@ impl<'a> Checker<'a> {
}
}
if let Some(binding) = scope.values.get_mut(id) {
binding.used = Some(scope_id);
binding.used = Some((scope_id, expr.location));
return;
}
@@ -1117,6 +1145,10 @@ impl<'a> Checker<'a> {
}
if self.settings.select.contains(&CheckCode::F821) {
// Allow __path__.
if self.path.ends_with("__init__.py") && id == "__path__" {
return;
}
self.checks.push(Check::new(
CheckKind::UndefinedName(id.clone()),
expr.location,
@@ -1131,22 +1163,19 @@ impl<'a> Checker<'a> {
&self.scopes[*(self.scope_stack.last().expect("No current scope found."))];
if self.settings.select.contains(&CheckCode::F823)
&& matches!(current.kind, ScopeKind::Function)
&& matches!(current.kind, ScopeKind::Function(_))
&& !current.values.contains_key(id)
{
for scope in self.scopes.iter().rev().skip(1) {
if matches!(scope.kind, ScopeKind::Function | ScopeKind::Module) {
let used = scope
.values
.get(id)
.map(|binding| binding.used)
.unwrap_or_default();
if let Some(scope_id) = used {
if scope_id == current.id {
self.checks.push(Check::new(
CheckKind::UndefinedLocal(id.clone()),
expr.location,
));
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if let Some(binding) = scope.values.get(id) {
if let Some((scope_id, location)) = binding.used {
if scope_id == current.id {
self.checks.push(Check::new(
CheckKind::UndefinedLocal(id.clone()),
location,
));
}
}
}
}
@@ -1240,12 +1269,12 @@ impl<'a> Checker<'a> {
}
}
fn check_deferred_string_annotations<'b>(&mut self, path: &str, allocator: &'b mut Vec<Expr>)
fn check_deferred_string_annotations<'b>(&mut self, allocator: &'b mut Vec<Expr>)
where
'b: 'a,
{
while let Some((location, expression)) = self.deferred_string_annotations.pop() {
if let Ok(mut expr) = parser::parse_expression(expression, path) {
if let Ok(mut expr) = parser::parse_expression(expression, "<filename>") {
relocate_expr(&mut expr, location);
allocator.push(expr);
} else if self.settings.select.contains(&CheckCode::F722) {
@@ -1264,7 +1293,7 @@ impl<'a> Checker<'a> {
while let Some((stmt, scopes, parents)) = self.deferred_functions.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.push_scope(Scope::new(ScopeKind::Function));
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
match &stmt.node {
StmtKind::FunctionDef { body, args, .. }
@@ -1288,7 +1317,7 @@ impl<'a> Checker<'a> {
while let Some((expr, scopes, parents)) = self.deferred_lambdas.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.push_scope(Scope::new(ScopeKind::Function));
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
if let ExprKind::Lambda { args, body } = &expr.node {
self.visit_arguments(args);
@@ -1328,7 +1357,7 @@ impl<'a> Checker<'a> {
});
if self.settings.select.contains(&CheckCode::F822)
&& !Path::new(self.path).ends_with("__init__.py")
&& !self.path.ends_with("__init__.py")
{
if let Some(binding) = all_binding {
if let Some(names) = all_names {
@@ -1374,7 +1403,7 @@ pub fn check_ast(
content: &str,
settings: &Settings,
autofix: &fixer::Mode,
path: &str,
path: &Path,
) -> Vec<Check> {
let mut checker = Checker::new(settings, autofix, path, content);
checker.push_scope(Scope::new(ScopeKind::Module));
@@ -1391,7 +1420,7 @@ pub fn check_ast(
checker.check_deferred_assignments();
checker.check_deferred_annotations();
let mut allocator = vec![];
checker.check_deferred_string_annotations(path, &mut allocator);
checker.check_deferred_string_annotations(&mut allocator);
// Reset the scope to module-level, and check all consumed scopes.
checker.scope_stack = vec![GLOBAL_SCOPE_INDEX];

View File

@@ -35,7 +35,7 @@ pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings)
// Enforce line length.
if enforce_line_too_long {
let line_length = line.len();
let line_length = line.chars().count();
if should_enforce_line_length(line, line_length, settings.line_length) {
let check = Check::new(
CheckKind::LineTooLong(line_length, settings.line_length),
@@ -53,3 +53,28 @@ pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings)
}
checks.extend(line_checks);
}
#[cfg(test)]
mod tests {
use super::check_lines;
use super::*;
use std::collections::BTreeSet;
#[test]
fn e501_non_ascii_char() {
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
let check_with_max_line_length = |line_length: usize| {
let mut checks: Vec<Check> = vec![];
let settings = Settings {
line_length,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from_iter(vec![CheckCode::E501]),
};
check_lines(&mut checks, line, &settings);
return checks;
};
assert!(!check_with_max_line_length(6).is_empty());
assert!(check_with_max_line_length(7).is_empty());
}
}

View File

@@ -6,7 +6,52 @@ use regex::Regex;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub const ALL_CHECK_CODES: [CheckCode; 42] = [
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
CheckCode::F401,
CheckCode::F403,
CheckCode::F404,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
CheckCode::R001,
CheckCode::R002,
];
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash, PartialOrd, Ord)]
pub enum CheckCode {
E402,
E501,
@@ -160,6 +205,54 @@ impl CheckCode {
_ => &LintSource::AST,
}
}
/// A placeholder representation of the CheckKind for the check.
pub fn kind(&self) -> CheckKind {
match self {
CheckCode::E742 => CheckKind::AmbiguousClassName("...".to_string()),
CheckCode::E743 => CheckKind::AmbiguousFunctionName("...".to_string()),
CheckCode::E741 => CheckKind::AmbiguousVariableName("...".to_string()),
CheckCode::F631 => CheckKind::AssertTuple,
CheckCode::F701 => CheckKind::BreakOutsideLoop,
CheckCode::F702 => CheckKind::ContinueOutsideLoop,
CheckCode::F707 => CheckKind::DefaultExceptNotLast,
CheckCode::E731 => CheckKind::DoNotAssignLambda,
CheckCode::E722 => CheckKind::DoNotUseBareExcept,
CheckCode::F831 => CheckKind::DuplicateArgumentName,
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
CheckCode::F722 => CheckKind::ForwardAnnotationSyntaxError("...".to_string()),
CheckCode::F407 => CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckCode::E902 => CheckKind::IOError("...".to_string()),
CheckCode::F634 => CheckKind::IfTuple,
CheckCode::F406 => CheckKind::ImportStarNotPermitted("...".to_string()),
CheckCode::F403 => CheckKind::ImportStarUsage("...".to_string()),
CheckCode::F633 => CheckKind::InvalidPrintSyntax,
CheckCode::F632 => CheckKind::IsLiteral,
CheckCode::F404 => CheckKind::LateFutureImport,
CheckCode::E501 => CheckKind::LineTooLong(89, 88),
CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile,
CheckCode::F601 => CheckKind::MultiValueRepeatedKeyLiteral,
CheckCode::F602 => CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
CheckCode::R002 => CheckKind::NoAssertEquals,
CheckCode::E711 => CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckCode::E713 => CheckKind::NotInTest,
CheckCode::E714 => CheckKind::NotIsTest,
CheckCode::F901 => CheckKind::RaiseNotImplemented,
CheckCode::F706 => CheckKind::ReturnOutsideFunction,
CheckCode::E999 => CheckKind::SyntaxError("...".to_string()),
CheckCode::F621 => CheckKind::TooManyExpressionsInStarredAssignment,
CheckCode::E712 => CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckCode::F622 => CheckKind::TwoStarredExpressions,
CheckCode::E721 => CheckKind::TypeComparison,
CheckCode::F822 => CheckKind::UndefinedExport("...".to_string()),
CheckCode::F823 => CheckKind::UndefinedLocal("...".to_string()),
CheckCode::F821 => CheckKind::UndefinedName("...".to_string()),
CheckCode::F401 => CheckKind::UnusedImport("...".to_string()),
CheckCode::F841 => CheckKind::UnusedVariable("...".to_string()),
CheckCode::R001 => CheckKind::UselessObjectInheritance("...".to_string()),
CheckCode::F704 => CheckKind::YieldOutsideFunction,
}
}
}
#[allow(clippy::upper_case_acronyms)]

235
src/fs.rs
View File

@@ -1,27 +1,53 @@
use std::borrow::Cow;
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::Result;
use glob::Pattern;
use anyhow::{anyhow, Result};
use log::debug;
use path_absolutize::path_dedot;
use path_absolutize::Absolutize;
use walkdir::{DirEntry, WalkDir};
fn is_excluded(path: &Path, exclude: &[Pattern]) -> bool {
if let Some(file_name) = path.file_name() {
if let Some(file_name) = file_name.to_str() {
for pattern in exclude {
if pattern.matches(file_name) {
use crate::settings::FilePattern;
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
let file_path = path
.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
let file_basename = path
.file_name()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?
.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
Ok((file_path, file_basename))
}
fn is_excluded(file_path: &str, file_basename: &str, exclude: &[FilePattern]) -> bool {
for pattern in exclude {
match &pattern {
FilePattern::Simple(basename) => {
if *basename == file_basename {
return true;
}
}
FilePattern::Complex(absolute, basename) => {
if absolute.matches(file_path) {
return true;
}
if basename
.as_ref()
.map(|pattern| pattern.matches(file_basename))
.unwrap_or_default()
{
return true;
}
}
false
} else {
false
}
} else {
false
}
false
}
fn is_included(path: &Path) -> bool {
@@ -30,27 +56,53 @@ fn is_included(path: &Path) -> bool {
}
pub fn iter_python_files<'a>(
path: &'a PathBuf,
exclude: &'a [Pattern],
extend_exclude: &'a [Pattern],
path: &'a Path,
exclude: &'a [FilePattern],
extend_exclude: &'a [FilePattern],
) -> impl Iterator<Item = DirEntry> + 'a {
WalkDir::new(path)
// Run some checks over the provided patterns, to enable optimizations below.
let has_exclude = !exclude.is_empty();
let has_extend_exclude = !extend_exclude.is_empty();
let exclude_simple = exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
let extend_exclude_simple = exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
WalkDir::new(normalize_path(path))
.follow_links(true)
.into_iter()
.filter_entry(|entry| {
if exclude.is_empty() && extend_exclude.is_empty() {
.filter_entry(move |entry| {
if !has_exclude && !has_extend_exclude {
return true;
}
let path = entry.path();
if is_excluded(path, exclude) {
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if is_excluded(path, extend_exclude) {
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
match extract_path_names(path) {
Ok((file_path, file_basename)) => {
let file_type = entry.file_type();
if has_exclude
&& (!exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, exclude)
{
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)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
}
}
Err(_) => {
debug!("Ignored path due to error in parsing: {:?}", path);
true
}
}
})
.filter_map(|entry| entry.ok())
@@ -60,6 +112,37 @@ pub fn iter_python_files<'a>(
})
}
/// Convert any path to an absolute path (based on the current working directory).
pub fn normalize_path(path: &Path) -> PathBuf {
if path == Path::new(".") || path == Path::new("..") {
return path.to_path_buf();
}
if let Ok(path) = path.absolutize() {
return path.to_path_buf();
}
path.to_path_buf()
}
/// Convert any path to an absolute path (based on the specified project root).
pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
if path == Path::new(".") || path == Path::new("..") {
return path.to_path_buf();
}
if let Ok(path) = path.absolutize_from(project_root) {
return path.to_path_buf();
}
path.to_path_buf()
}
/// Convert an absolute path to be relative to the current working directory.
pub fn relativize_path(path: &Path) -> Cow<str> {
if let Ok(path) = path.strip_prefix(path_dedot::CWD.deref()) {
return path.to_string_lossy();
}
path.to_string_lossy()
}
/// Read a file's contents from disk.
pub fn read_file(path: &Path) -> Result<String> {
let file = File::open(path)?;
let mut buf_reader = BufReader::new(file);
@@ -72,41 +155,95 @@ pub fn read_file(path: &Path) -> Result<String> {
mod tests {
use std::path::Path;
use glob::Pattern;
use anyhow::Result;
use path_absolutize::Absolutize;
use crate::fs::{is_excluded, is_included};
use crate::fs::{extract_path_names, is_excluded, is_included};
use crate::settings::FilePattern;
#[test]
fn inclusions() {
let path = Path::new("foo/bar/baz.py");
assert!(is_included(path));
let path = Path::new("foo/bar/baz.py").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.pyi");
assert!(is_included(path));
let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.js");
assert!(!is_included(path));
let path = Path::new("foo/bar/baz.js").absolutize().unwrap();
assert!(!is_included(&path));
let path = Path::new("foo/bar/baz");
assert!(!is_included(path));
let path = Path::new("foo/bar/baz").absolutize().unwrap();
assert!(!is_included(&path));
}
#[test]
fn exclusions() {
let path = Path::new("foo");
let exclude = vec![Pattern::new("foo").unwrap()];
assert!(is_excluded(path, &exclude));
fn exclusions() -> Result<()> {
let project_root = Path::new("/tmp/");
let path = Path::new("foo/bar");
let exclude = vec![Pattern::new("bar").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("baz.py").unwrap()];
assert!(is_excluded(path, &exclude));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"bar",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py");
let exclude = vec![Pattern::new("baz").unwrap()];
assert!(!is_excluded(path, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/baz.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/*.py",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(file_path, file_basename, &exclude));
Ok(())
}
}

View File

@@ -2,7 +2,7 @@ extern crate core;
mod ast;
mod autofix;
mod cache;
pub mod cache;
pub mod check_ast;
mod check_lines;
pub mod checks;
@@ -10,6 +10,7 @@ pub mod fs;
pub mod linter;
pub mod logging;
pub mod message;
mod pyproject;
pub mod printer;
pub mod pyproject;
mod python;
pub mod settings;

View File

@@ -13,10 +13,12 @@ use crate::message::Message;
use crate::settings::Settings;
use crate::{cache, fs};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
// Read the file from disk.
let contents = fs::read_file(path)?;
fn check_path(
path: &Path,
contents: &str,
settings: &Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
@@ -26,13 +28,12 @@ fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
{
let path = path.to_string_lossy();
let python_ast = parser::parse_program(&contents, &path)?;
checks.extend(check_ast(&python_ast, &contents, settings, autofix, &path));
let python_ast = parser::parse_program(contents, "<filename>")?;
checks.extend(check_ast(&python_ast, contents, settings, autofix, path));
}
// Run the lines-based checks.
check_lines(&mut checks, &contents, settings);
check_lines(&mut checks, contents, settings);
Ok(checks)
}
@@ -55,7 +56,7 @@ pub fn lint_path(
let contents = fs::read_file(path)?;
// Generate checks.
let mut checks = check_path(path, settings, autofix)?;
let mut checks = check_path(path, &contents, settings, autofix)?;
// Apply autofix.
if matches!(autofix, fixer::Mode::Apply) {
@@ -85,10 +86,20 @@ mod tests {
use anyhow::Result;
use crate::autofix::fixer;
use crate::checks::CheckCode;
use crate::linter::check_path;
use crate::checks::{Check, CheckCode};
use crate::fs;
use crate::linter;
use crate::settings;
fn check_path(
path: &Path,
settings: &settings::Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
linter::check_path(path, &contents, settings, autofix)
}
#[test]
fn e402() -> Result<()> {
let mut checks = check_path(
@@ -752,6 +763,23 @@ mod tests {
Ok(())
}
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings {
line_length: 88,
exclude: vec![],
extend_exclude: vec![],
select: BTreeSet::from([CheckCode::F821, CheckCode::F822]),
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn future_annotations() -> Result<()> {
let mut checks = check_path(

View File

@@ -1,3 +1,5 @@
extern crate core;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::mpsc::channel;
@@ -6,19 +8,21 @@ use std::time::Instant;
use anyhow::Result;
use clap::{Parser, ValueHint};
use colored::Colorize;
use glob::Pattern;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
use rayon::prelude::*;
use walkdir::DirEntry;
use ::ruff::cache;
use ::ruff::checks::CheckCode;
use ::ruff::checks::CheckKind;
use ::ruff::fs::iter_python_files;
use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging;
use ::ruff::message::Message;
use ::ruff::settings::Settings;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::pyproject;
use ::ruff::settings::{FilePattern, Settings};
use ::ruff::tell_user;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
@@ -27,6 +31,7 @@ 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)]
struct Cli {
#[clap(parse(from_os_str), value_hint = ValueHint::AnyPath, required = true)]
files: Vec<PathBuf>,
@@ -56,10 +61,13 @@ struct Cli {
ignore: Vec<CheckCode>,
/// List of paths, used to exclude files and/or directories from checks.
#[clap(long, multiple = true)]
exclude: Vec<Pattern>,
exclude: Vec<String>,
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
#[clap(long, multiple = true)]
extend_exclude: Vec<Pattern>,
extend_exclude: Vec<String>,
/// Output serialization format for error messages.
#[clap(long, arg_enum, default_value_t=SerializationFormat::Text)]
format: SerializationFormat,
}
#[cfg(feature = "update-informer")]
@@ -142,60 +150,25 @@ fn run_once(
Ok(messages)
}
fn report_once(messages: &[Message]) -> Result<()> {
let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) =
messages.iter().partition(|message| message.fixed);
let num_fixable = outstanding
.iter()
.filter(|message| message.kind.fixable())
.count();
if !outstanding.is_empty() {
for message in &outstanding {
println!("{}", message);
}
println!();
}
if !fixed.is_empty() {
println!(
"Found {} error(s) ({} fixed).",
outstanding.len(),
fixed.len()
);
} else {
println!("Found {} error(s).", outstanding.len());
}
if num_fixable > 0 {
println!("{num_fixable} potentially fixable with the --fix option.");
}
Ok(())
}
fn report_continuously(messages: &[Message]) -> Result<()> {
tell_user!(
"Found {} error(s). Watching for file changes.",
messages.len(),
);
if !messages.is_empty() {
println!();
for message in messages {
println!("{}", message);
}
}
Ok(())
}
fn inner_main() -> Result<ExitCode> {
let cli = Cli::parse();
set_up_logging(cli.verbose)?;
let mut settings = Settings::from_paths(&cli.files);
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&cli.files);
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(&project_root);
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
// Parse the settings from the pyproject.toml and command-line arguments.
let mut settings = Settings::from_pyproject(&pyproject, &project_root);
if !cli.select.is_empty() {
settings.select(cli.select);
}
@@ -203,24 +176,39 @@ fn inner_main() -> Result<ExitCode> {
settings.ignore(&cli.ignore);
}
if !cli.exclude.is_empty() {
settings.exclude = cli.exclude;
settings.exclude = cli
.exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
}
if !cli.extend_exclude.is_empty() {
settings.extend_exclude = cli.extend_exclude;
settings.extend_exclude = cli
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, &project_root))
.collect();
}
cache::init()?;
let mut printer = Printer::new(cli.format);
if cli.watch {
if cli.fix {
println!("Warning: --fix is not enabled in watch mode.")
println!("Warning: --fix is not enabled in watch mode.");
}
if cli.format != SerializationFormat::Text {
println!("Warning: --format 'text' is used in watch mode.");
}
// Perform an initial run instantly.
clearscreen::clear()?;
printer.clear_screen()?;
tell_user!("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
report_continuously(&messages)?;
printer.write_continuously(&messages)?;
}
// Configure the file watcher.
@@ -235,12 +223,12 @@ fn inner_main() -> Result<ExitCode> {
Ok(e) => {
if let Some(path) = e.path {
if path.to_string_lossy().ends_with(".py") {
clearscreen::clear()?;
printer.clear_screen()?;
tell_user!("File change detected...\n");
let messages = run_once(&cli.files, &settings, !cli.no_cache, false)?;
if !cli.quiet {
report_continuously(&messages)?;
printer.write_continuously(&messages)?;
}
}
}
@@ -251,7 +239,7 @@ fn inner_main() -> Result<ExitCode> {
} else {
let messages = run_once(&cli.files, &settings, !cli.no_cache, cli.fix)?;
if !cli.quiet {
report_once(&messages)?;
printer.write_once(&messages)?;
}
#[cfg(feature = "update-informer")]

View File

@@ -1,11 +1,13 @@
use std::cmp::Ordering;
use std::fmt;
use std::path::Path;
use colored::Colorize;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use crate::checks::CheckKind;
use crate::fs::relativize_path;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
@@ -36,7 +38,8 @@ impl fmt::Display for Message {
write!(
f,
"{}{}{}{}{}{} {} {}",
self.filename.white().bold(),
relativize_path(Path::new(&self.filename)).white().bold(),
// self.filename.white(),
":".cyan(),
self.location.row(),
":".cyan(),

80
src/printer.rs Normal file
View File

@@ -0,0 +1,80 @@
use colored::Colorize;
use anyhow::Result;
use clap::ValueEnum;
use crate::message::Message;
use crate::tell_user;
#[derive(Clone, Copy, ValueEnum, PartialEq, Eq, Debug)]
pub enum SerializationFormat {
Text,
Json,
}
pub struct Printer {
format: SerializationFormat,
}
impl Printer {
pub fn new(format: SerializationFormat) -> Self {
Self { format }
}
pub fn write_once(&mut self, messages: &[Message]) -> Result<()> {
let (fixed, outstanding): (Vec<&Message>, Vec<&Message>) =
messages.iter().partition(|message| message.fixed);
let num_fixable = outstanding
.iter()
.filter(|message| message.kind.fixable())
.count();
match self.format {
SerializationFormat::Json => {
println!("{}", serde_json::to_string_pretty(&messages)?)
}
SerializationFormat::Text => {
if !fixed.is_empty() {
println!(
"Found {} error(s) ({} fixed).",
outstanding.len(),
fixed.len()
)
} else {
println!("Found {} error(s).", outstanding.len())
}
for message in outstanding {
println!("{}", message)
}
if num_fixable > 0 {
println!("{num_fixable} potentially fixable with the --fix option.")
}
}
}
Ok(())
}
pub fn write_continuously(&mut self, messages: &[Message]) -> Result<()> {
tell_user!(
"Found {} error(s). Watching for file changes.",
messages.len(),
);
if !messages.is_empty() {
println!();
for message in messages {
println!("{}", message)
}
}
Ok(())
}
pub fn clear_screen(&mut self) -> Result<()> {
clearscreen::clear()?;
Ok(())
}
}

View File

@@ -1,33 +1,32 @@
use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::Result;
use common_path::common_path_all;
use log::debug;
use path_absolutize::path_dedot;
use serde::Deserialize;
use crate::checks::CheckCode;
use crate::fs;
pub fn load_config(paths: &[PathBuf]) -> Config {
match find_project_root(paths) {
Some(project_root) => match find_pyproject_toml(&project_root) {
Some(path) => {
debug!("Found pyproject.toml at: {:?}", path);
match parse_pyproject_toml(&path) {
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()
}
}
pub fn load_config(pyproject: &Option<PathBuf>) -> 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()
}
None => Default::default(),
},
None => Default::default(),
None => {
println!("No pyproject.toml found.");
println!("Falling back to default configuration...");
Default::default()
}
}
}
@@ -35,8 +34,8 @@ pub fn load_config(paths: &[PathBuf]) -> Config {
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<PathBuf>>,
pub extend_exclude: Option<Vec<PathBuf>>,
pub exclude: Option<Vec<String>>,
pub extend_exclude: Option<Vec<String>>,
pub select: Option<Vec<CheckCode>>,
pub ignore: Option<Vec<CheckCode>>,
}
@@ -56,20 +55,32 @@ fn parse_pyproject_toml(path: &Path) -> Result<PyProject> {
toml::from_str(&contents).map_err(|e| e.into())
}
fn find_pyproject_toml(path: &Path) -> Option<PathBuf> {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
return Some(path_pyproject_toml);
pub fn find_pyproject_toml(path: &Option<PathBuf>) -> Option<PathBuf> {
if let Some(path) = path {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
return Some(path_pyproject_toml);
}
}
find_user_pyproject_toml()
}
fn find_user_pyproject_toml() -> Option<PathBuf> {
dirs::home_dir().map(|path| path.join(".ruff"))
let mut path = dirs::config_dir()?;
path.push("ruff");
path.push("pyproject.toml");
if path.is_file() {
Some(path)
} else {
None
}
}
fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
if let Some(prefix) = common_path_all(sources.iter().map(PathBuf::as_path)) {
pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
let cwd = path_dedot::CWD.deref();
let absolute_sources: Vec<PathBuf> = sources.iter().map(|source| cwd.join(source)).collect();
if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
for directory in prefix.ancestors() {
if directory.join(".git").is_dir() {
return Some(directory.to_path_buf());
@@ -88,7 +99,8 @@ fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::env::current_dir;
use std::path::PathBuf;
use anyhow::Result;
@@ -160,7 +172,7 @@ exclude = ["foo.py"]
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: Some(vec![Path::new("foo.py").to_path_buf()]),
exclude: Some(vec!["foo.py".to_string()]),
extend_exclude: None,
select: None,
ignore: None,
@@ -241,13 +253,15 @@ other-attribute = 1
#[test]
fn find_and_parse_pyproject_toml() -> Result<()> {
let cwd = current_dir()?;
let project_root =
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")])
.expect("Unable to find project root.");
assert_eq!(project_root, Path::new("resources/test/fixtures"));
assert_eq!(project_root, cwd.join("resources/test/fixtures"));
let path = find_pyproject_toml(&project_root).expect("Unable to find pyproject.toml.");
assert_eq!(path, Path::new("resources/test/fixtures/pyproject.toml"));
let path =
find_pyproject_toml(&Some(project_root)).expect("Unable to find pyproject.toml.");
assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
@@ -260,53 +274,11 @@ other-attribute = 1
line_length: Some(88),
exclude: None,
extend_exclude: Some(vec![
Path::new("excluded.py").to_path_buf(),
Path::new("migrations").to_path_buf()
]),
select: Some(vec![
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
CheckCode::F401,
CheckCode::F403,
CheckCode::F404,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
CheckCode::R001,
CheckCode::R002,
"excluded.py".to_string(),
"migrations".to_string(),
"directory/also_excluded.py".to_string(),
]),
select: None,
ignore: None,
}
);

View File

@@ -1,18 +1,44 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use glob::Pattern;
use once_cell::sync::Lazy;
use crate::checks::CheckCode;
use crate::checks::{CheckCode, ALL_CHECK_CODES};
use crate::fs;
use crate::pyproject::load_config;
#[derive(Debug, Clone)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern.");
let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
Some(Pattern::new(pattern).expect("Invalid pattern."))
} else {
None
};
FilePattern::Complex(absolute, basename)
}
}
#[derive(Debug)]
pub struct Settings {
pub line_length: usize,
pub exclude: Vec<Pattern>,
pub extend_exclude: Vec<Pattern>,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub select: BTreeSet<CheckCode>,
}
@@ -24,42 +50,42 @@ impl Hash for Settings {
}
}
}
static DEFAULT_EXCLUDE: Lazy<Vec<Pattern>> = Lazy::new(|| {
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
Pattern::new(".bzr").unwrap(),
Pattern::new(".direnv").unwrap(),
Pattern::new(".eggs").unwrap(),
Pattern::new(".git").unwrap(),
Pattern::new(".hg").unwrap(),
Pattern::new(".mypy_cache").unwrap(),
Pattern::new(".nox").unwrap(),
Pattern::new(".pants.d").unwrap(),
Pattern::new(".svn").unwrap(),
Pattern::new(".tox").unwrap(),
Pattern::new(".venv").unwrap(),
Pattern::new("__pypackages__").unwrap(),
Pattern::new("_build").unwrap(),
Pattern::new("buck-out").unwrap(),
Pattern::new("build").unwrap(),
Pattern::new("dist").unwrap(),
Pattern::new("node_modules").unwrap(),
Pattern::new("venv").unwrap(),
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
]
});
impl Settings {
pub fn from_paths(paths: &[PathBuf]) -> Self {
let config = load_config(paths);
pub fn from_pyproject(path: &Option<PathBuf>, project_root: &Option<PathBuf>) -> Self {
let config = load_config(path);
let mut settings = Settings {
line_length: config.line_length.unwrap_or(88),
exclude: config
.exclude
.map(|paths| {
paths
.into_iter()
.map(|path| {
Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.")
})
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
@@ -67,59 +93,16 @@ impl Settings {
.extend_exclude
.map(|paths| {
paths
.into_iter()
.map(|path| {
Pattern::new(&path.to_string_lossy()).expect("Invalid pattern.")
})
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.unwrap_or_default(),
select: BTreeSet::from_iter(config.select.unwrap_or_else(|| {
vec![
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
CheckCode::E712,
CheckCode::E713,
CheckCode::E714,
CheckCode::E721,
CheckCode::E722,
CheckCode::E731,
CheckCode::E741,
CheckCode::E742,
CheckCode::E743,
CheckCode::E902,
CheckCode::E999,
CheckCode::F401,
CheckCode::F403,
CheckCode::F406,
CheckCode::F407,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
CheckCode::F621,
CheckCode::F622,
CheckCode::F631,
CheckCode::F632,
CheckCode::F633,
CheckCode::F634,
CheckCode::F701,
CheckCode::F702,
CheckCode::F704,
CheckCode::F706,
CheckCode::F707,
CheckCode::F722,
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F901,
// Disable refactoring codes by default.
// CheckCode::R001,
// CheckCode::R002,
]
})),
select: if let Some(select) = config.select {
BTreeSet::from_iter(select)
} else {
BTreeSet::from_iter(ALL_CHECK_CODES)
},
};
if let Some(ignore) = &config.ignore {
settings.ignore(ignore);

View File

@@ -32,4 +32,10 @@ expression: checks
row: 58
column: 5
fix: ~
- kind:
UndefinedName: TOMATO
location:
row: 83
column: 11
fix: ~

View File

@@ -0,0 +1,6 @@
---
source: src/linter.rs
expression: checks
---
[]