Compare commits

...

9 Commits

Author SHA1 Message Date
Charlie Marsh
050f34dd25 Bump version to 0.0.104 2022-11-06 15:31:10 -05:00
Charlie Marsh
1cd82d588b Categorize functions in pep8-naming (#624) 2022-11-06 15:29:49 -05:00
Charlie Marsh
cea9e34942 Update CONTRIBUTING.md (#623) 2022-11-06 14:31:16 -05:00
Reiner Gerecke
1ede377402 Improve discoverability of dev commands (#621) 2022-11-06 14:25:59 -05:00
Reiner Gerecke
82eff641fb Remove utf-8 encoding declaration (#618) 2022-11-06 14:23:06 -05:00
Charlie Marsh
eb1bc9f092 Allow underscore names in N803 (#622) 2022-11-06 14:19:02 -05:00
Reiner Gerecke
df88504dea pyflakes F632 Autofix (#612) 2022-11-06 06:57:46 -05:00
Anders Kaseorg
b9ec3e9137 Correct source link in CONFUSABLES comment (#617) 2022-11-05 20:53:05 -04:00
Anders Kaseorg
b067b665ff Fix B015 false positive on comparison deep inside expression statement (#616) 2022-11-05 20:13:22 -04:00
44 changed files with 684 additions and 311 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[alias]
dev = "run --package ruff_dev --bin ruff_dev"

View File

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

View File

@@ -1,17 +1,17 @@
# Contributing to ruff
# Contributing to Ruff
Welcome! We're happy to have you here. Thank you in advance for your contribution to ruff.
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
## The basics
ruff welcomes contributions in the form of Pull Requests. For small changes (e.g., bug fixes), feel
Ruff welcomes contributions in the form of Pull Requests. For small changes (e.g., bug fixes), feel
free to submit a PR. For larger changes (e.g., new lint rules, new functionality, new configuration
options), consider submitting an [Issue](https://github.com/charliermarsh/ruff/issues) outlining
your proposed change.
### Prerequisites
ruff is written in Rust. You'll need to install the
Ruff is written in Rust. You'll need to install the
[Rust toolchain](https://www.rust-lang.org/tools/install) for development.
You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
@@ -22,7 +22,7 @@ cargo install cargo-insta
### Development
After cloning the repository, run ruff locally with:
After cloning the repository, run Ruff locally with:
```shell
cargo run resources/test/fixtures --no-cache
@@ -32,9 +32,9 @@ Prior to opening a pull request, ensure that your code has been auto-formatted,
both the lint and test validation checks:
```shell
cargo fmt # Auto-formatting...
cargo clippy # Linting...
cargo test # Testing...
cargo +nightly fmt --all # Auto-formatting...
cargo +nightly clippy --all # Linting...
cargo +nightly test --all # Testing...
```
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
@@ -45,12 +45,13 @@ prior to merging.
### Example: Adding a new lint rule
There are three phases to adding a new lint rule:
There are four phases to adding a new lint rule:
1. Define the rule in `src/checks.rs`.
2. Define the _logic_ for triggering the rule in `src/check_ast.rs` (for AST-based checks)
or `src/check_lines.rs` (for text-based checks).
2. Define the _logic_ for triggering the rule in `src/check_ast.rs` (for AST-based checks),
`src/check_tokens.rs` (for token-based checks), or `src/check_lines.rs` (for text-based checks).
3. Add a test fixture.
4. Update the generated files (documentation and generated code).
To define the rule, open up `src/checks.rs`. You'll need to define both a `CheckCode` and
`CheckKind`. As an example, you can grep for `E402` and `ModuleImportNotAtTopOfFile`, and follow the
@@ -58,37 +59,33 @@ pattern implemented therein.
To trigger the rule, you'll likely want to augment the logic in `src/check_ast.rs`, which defines
the Python AST visitor, responsible for iterating over the abstract syntax tree and collecting
lint-rule violations as it goes. Grep for the `Check::new` invocations to understand how other,
similar rules are implemented.
lint-rule violations as it goes. If you need to inspect the AST, you can run `cargo dev print-ast`
with a Python file. Grep for the `Check::new` invocations to understand how other, similar rules
are implemented.
To add a test fixture, create a file under `resources/test/fixtures`, named to match the `CheckCode`
you defined earlier (e.g., `E402.py`). This file should contain a variety of violations and
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py`. Once you're satisfied with the output,
codify the behavior as a snapshot test by adding a new function to the `mod tests` section of
`src/linter.rs`, like so:
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run Ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py --no-cache`. Once you're satisfied with the
output, codify the behavior as a snapshot test by adding a new `testcase` macro to the `mod tests`
section of `src/linter.rs`, like so:
```rust
#[test]
fn e402() -> Result<()> {
let mut checks = check_path(
Path::new("./resources/test/fixtures/E402.py"),
&settings::Settings::for_rule(CheckCode::E402),
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test_case(CheckCode::A001, Path::new("A001.py"); "A001")]
...
```
Then, run `cargo test`. Your test will fail, but you'll be prompted to follow-up with
`cargo insta review`. Accept the generated snapshot, then commit the snapshot file alongside the
rest of your changes.
Finally, to update the documentation, run `cargo dev generate-rules-table` from the repo root. To
update the generated prefix map, run `cargo dev generate-check-code-prefix`. Both of these commands
should be run whenever a new check is added to the codebase.
### Example: Adding a new configuration option
ruff's user-facing settings live in two places: first, the command-line options defined with
Ruff's user-facing settings live in two places: first, the command-line options defined with
[clap](https://docs.rs/clap/latest/clap/) via the `Cli` struct in `src/main.rs`; and second, the
`Config` struct defined `src/pyproject.rs`, which is responsible for extracting user-defined
settings from a `pyproject.toml` file.
@@ -103,9 +100,9 @@ acceptable unused variables (e.g., `_`).
## Release process
As of now, ruff has an ad hoc release process: releases are cut with high frequency via GitHub
As of now, Ruff has an ad hoc release process: releases are cut with high frequency via GitHub
Actions, which automatically generates the appropriate wheels across architectures and publishes
them to [PyPI](https://pypi.org/project/ruff/).
ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).

6
Cargo.lock generated
View File

@@ -920,7 +920,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.103-dev.0"
version = "0.0.104-dev.0"
dependencies = [
"anyhow",
"clap 4.0.15",
@@ -2221,7 +2221,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.103"
version = "0.0.104"
dependencies = [
"anyhow",
"assert_cmd",
@@ -2266,7 +2266,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.103"
version = "0.0.104"
dependencies = [
"anyhow",
"clap 4.0.15",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.103"
version = "0.0.104"
edition = "2021"
[lib]

View File

@@ -93,7 +93,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.103
rev: v0.0.104
hooks:
- id: ruff
```
@@ -300,7 +300,7 @@ By default, Ruff enables all `E` and `F` error codes, which correspond to those
The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` command-line option.
<!-- Sections automatically generated by examples/generate_rules_table.rs. -->
<!-- Sections automatically generated by `cargo dev generate-rules-table`. -->
<!-- Begin auto-generated sections. -->
### Pyflakes
@@ -322,7 +322,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | |
| F622 | TwoStarredExpressions | Two starred expressions in assignment | |
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | |
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | |
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | 🛠 |
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | |
| F634 | IfTuple | If test is a tuple, which is always `True` | |
| F701 | BreakOutsideLoop | `break` outside loop | |
@@ -426,6 +426,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 |
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 |
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | 🛠 |
| U009 | PEP3120UnnecessaryCodingComment | utf-8 encoding declaration is unnecessary | 🛠 |
### pep8-naming
@@ -606,7 +607,7 @@ including:
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (15/32)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (10/34)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (11/34)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis Flake8:
@@ -630,7 +631,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (15/32)
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (10/34).
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (11/34).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.

View File

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

View File

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

View File

@@ -22,3 +22,6 @@ data = [x for x in [1, 2, 3] if x in (1, 2)]
class TestClass:
1 == 1
print(1 == 1)

View File

@@ -3,3 +3,6 @@ if x is "abc":
if 123 is not y:
pass
if "123" is x < 3:
pass

View File

@@ -1,7 +1,7 @@
def func(a, A):
return a, A
def func(_, a, A):
return _, a, A
class Class:
def method(self, a, A):
return a, A
def method(self, _, a, A):
return _, a, A

View File

@@ -1,19 +1,43 @@
from abc import ABCMeta
class Class:
@classmethod
def bad_class_method(this):
def bad_method(this):
pass
if False:
def extra_bad_method(this):
pass
def good_method(self):
pass
@classmethod
def good_class_method(cls):
pass
def method(self):
def class_method(cls):
pass
@staticmethod
def static_method(x):
return x
def __init__(self):
...
def __new__(cls, *args, **kwargs):
...
def __init_subclass__(self, default_name, **kwargs):
...
class MetaClass(ABCMeta):
def bad_method(self):
pass
def good_method(cls):
pass
def func(x):
return x

View File

@@ -1,11 +1,11 @@
import random
from abc import ABCMeta
class Class:
def bad_method(this):
pass
if random.random(0, 2) == 0:
if False:
def extra_bad_method(this):
pass
@@ -21,6 +21,23 @@ class Class:
def static_method(x):
return x
def __init__(self):
...
def __new__(cls, *args, **kwargs):
...
def __init_subclass__(self, default_name, **kwargs):
...
class MetaClass(ABCMeta):
def bad_method(self):
pass
def good_method(cls):
pass
def func(x):
return x

3
resources/test/fixtures/U009_0.py vendored Normal file
View File

@@ -0,0 +1,3 @@
# coding=utf8
print('Hello world')

4
resources/test/fixtures/U009_1.py vendored Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
print('Hello world')

5
resources/test/fixtures/U009_2.py vendored Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/python
# A coding comment is only valid in the first two lines, so this one is not checked.
# -*- coding: utf-8 -*-
print('Hello world')

4
resources/test/fixtures/U009_3.py vendored Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
# -*- coding: something-else -*-
print('Hello world')

View File

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

View File

@@ -11,7 +11,7 @@ use itertools::Itertools;
use ruff::checks::CheckCode;
use strum::IntoEnumIterator;
const FILE: &str = "../src/checks_gen.rs";
const FILE: &str = "src/checks_gen.rs";
#[derive(Parser)]
#[command(author, version, about, long_about = None)]

View File

@@ -3,13 +3,13 @@
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use ruff::checks::{CheckCategory, CheckCode};
use strum::IntoEnumIterator;
const FILE: &str = "../README.md";
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
@@ -64,7 +64,11 @@ pub fn main(cli: &Cli) -> Result<()> {
print!("{}", output);
} else {
// Read the existing file.
let existing = fs::read_to_string(FILE)?;
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
@@ -79,7 +83,7 @@ pub fn main(cli: &Cli) -> Result<()> {
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(FILE)?;
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
write!(f, "{}\n\n", prefix)?;
write!(f, "{}", output)?;
write!(f, "{}", suffix)?;

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use rustpython_ast::{Expr, Keyword};
use rustpython_parser::ast::{Located, Location};
fn id() -> usize {
@@ -30,9 +31,17 @@ pub struct FunctionScope {
pub uses_locals: bool,
}
#[derive(Clone, Debug, Default)]
pub struct ClassScope<'a> {
pub name: &'a str,
pub bases: &'a [Expr],
pub keywords: &'a [Keyword],
pub decorator_list: &'a [Expr],
}
#[derive(Clone, Debug)]
pub enum ScopeKind {
Class,
pub enum ScopeKind<'a> {
Class(ClassScope<'a>),
Function(FunctionScope),
Generator,
Module,
@@ -40,15 +49,15 @@ pub enum ScopeKind {
}
#[derive(Clone, Debug)]
pub struct Scope {
pub struct Scope<'a> {
pub id: usize,
pub kind: ScopeKind,
pub kind: ScopeKind<'a>,
pub import_starred: bool,
pub values: BTreeMap<String, Binding>,
}
impl Scope {
pub fn new(kind: ScopeKind) -> Self {
impl<'a> Scope<'a> {
pub fn new(kind: ScopeKind<'a>) -> Self {
Scope {
id: id(),
kind,

View File

@@ -15,8 +15,8 @@ use crate::ast::helpers::{extract_handler_names, match_name_or_attr_from_module}
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingContext, BindingKind, CheckLocator, FunctionScope, ImportKind, Range, Scope,
ScopeKind,
Binding, BindingContext, BindingKind, CheckLocator, ClassScope, FunctionScope, ImportKind,
Range, Scope, ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{helpers, operations, visitor};
@@ -66,7 +66,7 @@ pub struct Checker<'a> {
// at various points in time.
pub(crate) parents: Vec<&'a Stmt>,
pub(crate) parent_stack: Vec<usize>,
scopes: Vec<Scope>,
scopes: Vec<Scope<'a>>,
scope_stack: Vec<usize>,
dead_scopes: Vec<usize>,
deferred_string_annotations: Vec<(Range, &'a str)>,
@@ -274,6 +274,7 @@ where
if let Some(check) =
pep8_naming::checks::invalid_first_argument_name_for_class_method(
self.current_scope(),
name,
decorator_list,
args,
&self.settings.pep8_naming,
@@ -286,6 +287,7 @@ where
if self.settings.enabled.contains(&CheckCode::N805) {
if let Some(check) = pep8_naming::checks::invalid_first_argument_name_for_method(
self.current_scope(),
name,
decorator_list,
args,
&self.settings.pep8_naming,
@@ -296,7 +298,7 @@ where
if self.settings.enabled.contains(&CheckCode::N807) {
if let Some(check) =
pep8_naming::checks::dunder_function_name(stmt, self.current_scope(), name)
pep8_naming::checks::dunder_function_name(self.current_scope(), stmt, name)
{
self.checks.push(check);
}
@@ -360,7 +362,7 @@ where
if self.settings.enabled.contains(&CheckCode::F706) {
if let Some(scope_index) = self.scope_stack.last().cloned() {
match self.scopes[scope_index].kind {
ScopeKind::Class | ScopeKind::Module => {
ScopeKind::Class(_) | ScopeKind::Module => {
self.checks.push(Check::new(
CheckKind::ReturnOutsideFunction,
self.locate_check(Range::from_located(stmt)),
@@ -377,7 +379,6 @@ where
keywords,
decorator_list,
body,
..
} => {
if self.settings.enabled.contains(&CheckCode::U004) {
pyupgrade::plugins::useless_object_inheritance(
@@ -427,7 +428,12 @@ where
for expr in decorator_list {
self.visit_expr(expr)
}
self.push_scope(Scope::new(ScopeKind::Class))
self.push_scope(Scope::new(ScopeKind::Class(ClassScope {
name,
bases,
keywords,
decorator_list,
})))
}
StmtKind::Import { names } => {
if self.settings.enabled.contains(&CheckCode::E402) {
@@ -809,6 +815,11 @@ where
}
}
StmtKind::Delete { .. } => {}
StmtKind::Expr { value, .. } => {
if self.settings.enabled.contains(&CheckCode::B015) {
flake8_bugbear::plugins::useless_comparison(self, value)
}
}
_ => {}
}
@@ -1251,7 +1262,7 @@ where
ExprKind::Yield { .. } | ExprKind::YieldFrom { .. } | ExprKind::Await { .. } => {
let scope = self.current_scope();
if self.settings.enabled.contains(&CheckCode::F704) {
if matches!(scope.kind, ScopeKind::Class | ScopeKind::Module) {
if matches!(scope.kind, ScopeKind::Class(_) | ScopeKind::Module) {
self.checks.push(Check::new(
CheckKind::YieldOutsideFunction,
self.locate_check(Range::from_located(expr)),
@@ -1319,12 +1330,13 @@ where
}
if self.settings.enabled.contains(&CheckCode::F632) {
self.checks.extend(pyflakes::checks::is_literal(
pyflakes::plugins::invalid_literal_comparison(
self,
left,
ops,
comparators,
self.locate_check(Range::from_located(expr)),
));
);
}
if self.settings.enabled.contains(&CheckCode::E721) {
@@ -1334,12 +1346,6 @@ where
self.locate_check(Range::from_located(expr)),
));
}
if self.settings.enabled.contains(&CheckCode::B015) {
if let Some(parent) = self.parents.last() {
flake8_bugbear::plugins::useless_comparison(self, left, parent);
}
}
}
ExprKind::Constant {
value: Constant::Str(value),
@@ -1717,7 +1723,7 @@ where
if self.settings.enabled.contains(&CheckCode::N803) {
if let Some(check) =
pep8_naming::checks::invalid_argument_name(Range::from_located(arg), &arg.node.arg)
pep8_naming::checks::invalid_argument_name(&arg.node.arg, Range::from_located(arg))
{
self.checks.push(check);
}
@@ -1791,7 +1797,7 @@ impl<'a> Checker<'a> {
.expect("Attempted to pop without scope.");
}
fn push_scope(&mut self, scope: Scope) {
fn push_scope(&mut self, scope: Scope<'a>) {
self.scope_stack.push(self.scopes.len());
self.scopes.push(scope);
}
@@ -1896,7 +1902,7 @@ impl<'a> Checker<'a> {
let mut import_starred = false;
for scope_index in self.scope_stack.iter().rev() {
let scope = &mut self.scopes[*scope_index];
if matches!(scope.kind, ScopeKind::Class) {
if matches!(scope.kind, ScopeKind::Class(_)) {
if id == "__class__" {
return;
} else if !first_iter && !in_generator {
@@ -2433,7 +2439,7 @@ impl<'a> Checker<'a> {
}
fn check_builtin_shadowing(&mut self, name: &str, location: Range, is_attribute: bool) {
if is_attribute && matches!(self.current_scope().kind, ScopeKind::Class) {
if is_attribute && matches!(self.current_scope().kind, ScopeKind::Class(_)) {
if self.settings.enabled.contains(&CheckCode::A003) {
if let Some(check) = flake8_builtins::checks::builtin_shadowing(
name,

View File

@@ -2,6 +2,8 @@
use std::collections::BTreeMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
@@ -11,6 +13,10 @@ use crate::noqa;
use crate::noqa::Directive;
use crate::settings::Settings;
// Regex from PEP263
static CODING_COMMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*utf-?8").expect("Invalid regex"));
/// Whether the given line is too long and should be reported.
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
if length > limit {
@@ -52,6 +58,27 @@ pub fn check_lines(
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
if lineno < 2 {
// PEP3120 makes utf-8 the default encoding.
if CODING_COMMENT_REGEX.is_match(line) {
let line_length = line.len();
let mut check = Check::new(
CheckKind::PEP3120UnnecessaryCodingComment,
Range {
location: Location::new(lineno + 1, 0),
end_location: Location::new(lineno + 1, line_length + 1),
},
);
if autofix.patch() {
check.amend(Fix::deletion(
Location::new(lineno + 1, 0),
Location::new(lineno + 1, line_length + 1),
));
}
line_checks.push(check);
}
}
if enforce_noqa {
noqa_directives
.entry(noqa_lineno)

View File

@@ -124,6 +124,7 @@ pub enum CheckCode {
U006,
U007,
U008,
U009,
// pydocstyle
D100,
D101,
@@ -365,6 +366,7 @@ pub enum CheckKind {
UsePEP585Annotation(String),
UsePEP604Annotation,
SuperCallWithParameters,
PEP3120UnnecessaryCodingComment,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -438,7 +440,9 @@ impl CheckCode {
/// physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 => &LintSource::Lines,
CheckCode::E501 | CheckCode::W292 | CheckCode::M001 | CheckCode::U009 => {
&LintSource::Lines
}
CheckCode::Q000
| CheckCode::Q001
| CheckCode::Q002
@@ -573,6 +577,7 @@ impl CheckCode {
CheckCode::U006 => CheckKind::UsePEP585Annotation("List".to_string()),
CheckCode::U007 => CheckKind::UsePEP604Annotation,
CheckCode::U008 => CheckKind::SuperCallWithParameters,
CheckCode::U009 => CheckKind::PEP3120UnnecessaryCodingComment,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -750,6 +755,7 @@ impl CheckCode {
CheckCode::U006 => CheckCategory::Pyupgrade,
CheckCode::U007 => CheckCategory::Pyupgrade,
CheckCode::U008 => CheckCategory::Pyupgrade,
CheckCode::U009 => CheckCategory::Pyupgrade,
CheckCode::D100 => CheckCategory::Pydocstyle,
CheckCode::D101 => CheckCategory::Pydocstyle,
CheckCode::D102 => CheckCategory::Pydocstyle,
@@ -918,6 +924,7 @@ impl CheckKind {
CheckKind::UsePEP604Annotation => &CheckCode::U007,
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
CheckKind::SuperCallWithParameters => &CheckCode::U008,
CheckKind::PEP3120UnnecessaryCodingComment => &CheckCode::U009,
// pydocstyle
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
@@ -1478,6 +1485,9 @@ impl CheckKind {
CheckKind::ErrorSuffixOnExceptionName(name) => {
format!("Exception name `{name}` should be named with an Error suffix")
}
CheckKind::PEP3120UnnecessaryCodingComment => {
"utf-8 encoding declaration is unnecessary".to_string()
}
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -1583,6 +1593,8 @@ impl CheckKind {
| CheckKind::UsePEP604Annotation
| CheckKind::UselessMetaclassType
| CheckKind::UselessObjectInheritance(_)
| CheckKind::PEP3120UnnecessaryCodingComment
| CheckKind::IsLiteral
)
}
}

View File

@@ -230,6 +230,7 @@ pub enum CheckCodePrefix {
U006,
U007,
U008,
U009,
W,
W2,
W29,
@@ -877,6 +878,7 @@ impl CheckCodePrefix {
CheckCode::U006,
CheckCode::U007,
CheckCode::U008,
CheckCode::U009,
],
CheckCodePrefix::U0 => vec![
CheckCode::U001,
@@ -887,6 +889,7 @@ impl CheckCodePrefix {
CheckCode::U006,
CheckCode::U007,
CheckCode::U008,
CheckCode::U009,
],
CheckCodePrefix::U00 => vec![
CheckCode::U001,
@@ -897,6 +900,7 @@ impl CheckCodePrefix {
CheckCode::U006,
CheckCode::U007,
CheckCode::U008,
CheckCode::U009,
],
CheckCodePrefix::U001 => vec![CheckCode::U001],
CheckCodePrefix::U002 => vec![CheckCode::U002],
@@ -906,6 +910,7 @@ impl CheckCodePrefix {
CheckCodePrefix::U006 => vec![CheckCode::U006],
CheckCodePrefix::U007 => vec![CheckCode::U007],
CheckCodePrefix::U008 => vec![CheckCode::U008],
CheckCodePrefix::U009 => vec![CheckCode::U009],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -1143,6 +1148,7 @@ impl CheckCodePrefix {
CheckCodePrefix::U006 => PrefixSpecificity::Explicit,
CheckCodePrefix::U007 => PrefixSpecificity::Explicit,
CheckCodePrefix::U008 => PrefixSpecificity::Explicit,
CheckCodePrefix::U009 => PrefixSpecificity::Explicit,
CheckCodePrefix::W => PrefixSpecificity::Category,
CheckCodePrefix::W2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::W29 => PrefixSpecificity::Tens,

View File

@@ -1,5 +1,5 @@
use anyhow::Result;
use libcst_native::Module;
use libcst_native::{Expr, Module, SmallStatement, Statement};
pub fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -7,3 +7,15 @@ pub fn match_module(module_text: &str) -> Result<Module> {
Err(_) => Err(anyhow::anyhow!("Failed to extract CST from source.")),
}
}
pub fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::Expr(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
Err(anyhow::anyhow!("Expected node to be: SmallStatement::Expr"))
}
} else {
Err(anyhow::anyhow!("Expected node to be: Statement::Simple"))
}
}

View File

@@ -1,14 +1,14 @@
use rustpython_ast::{Expr, Stmt, StmtKind};
use rustpython_ast::{Expr, ExprKind};
use crate::ast::types::{CheckLocator, Range};
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
pub fn useless_comparison(checker: &mut Checker, expr: &Expr, parent: &Stmt) {
if let StmtKind::Expr { .. } = &parent.node {
pub fn useless_comparison(checker: &mut Checker, expr: &Expr) {
if let ExprKind::Compare { left, .. } = &expr.node {
checker.add_check(Check::new(
CheckKind::UselessComparison,
checker.locate_check(Range::from_located(expr)),
checker.locate_check(Range::from_located(left)),
));
}
}

View File

@@ -1,28 +1,15 @@
use anyhow::Result;
use libcst_native::{
Arg, Call, Codegen, Dict, DictComp, DictElement, Element, Expr, Expression, LeftCurlyBrace,
LeftParen, LeftSquareBracket, List, ListComp, Module, Name, ParenthesizableWhitespace,
RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
SmallStatement, Statement, Tuple,
LeftParen, LeftSquareBracket, List, ListComp, Name, ParenthesizableWhitespace, RightCurlyBrace,
RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace, Tuple,
};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::cst::matchers::match_module;
use crate::cst::matchers::{match_expr, match_module};
use crate::source_code_locator::SourceCodeLocator;
fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::Expr(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
Err(anyhow::anyhow!("Expected node to be: SmallStatement::Expr"))
}
} else {
Err(anyhow::anyhow!("Expected node to be: Statement::Simple"))
}
}
fn match_call<'a, 'b>(expr: &'a mut Expr<'b>) -> Result<&'a mut Call<'b>> {
if let Expression::Call(call) = &mut expr.value {
Ok(call)

View File

@@ -439,6 +439,10 @@ mod tests {
#[test_case(CheckCode::U006, Path::new("U006.py"); "U006")]
#[test_case(CheckCode::U007, Path::new("U007.py"); "U007")]
#[test_case(CheckCode::U008, Path::new("U008.py"); "U008")]
#[test_case(CheckCode::U009, Path::new("U009_0.py"); "U009_0")]
#[test_case(CheckCode::U009, Path::new("U009_1.py"); "U009_1")]
#[test_case(CheckCode::U009, Path::new("U009_2.py"); "U009_2")]
#[test_case(CheckCode::U009, Path::new("U009_3.py"); "U009_3")]
#[test_case(CheckCode::W292, Path::new("W292_0.py"); "W292_0")]
#[test_case(CheckCode::W292, Path::new("W292_1.py"); "W292_1")]
#[test_case(CheckCode::W292, Path::new("W292_2.py"); "W292_2")]

View File

@@ -1,8 +1,9 @@
use itertools::Itertools;
use rustpython_ast::{Arguments, Expr, ExprKind, Stmt};
use crate::ast::types::{FunctionScope, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers;
use crate::pep8_naming::helpers::FunctionType;
use crate::pep8_naming::settings::Settings;
/// N801
@@ -25,7 +26,7 @@ pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
/// N802
pub fn invalid_function_name(func_def: &Stmt, name: &str, settings: &Settings) -> Option<Check> {
if !is_lower(name)
if name.to_lowercase() != name
&& !settings
.ignore_names
.iter()
@@ -40,8 +41,8 @@ pub fn invalid_function_name(func_def: &Stmt, name: &str, settings: &Settings) -
}
/// N803
pub fn invalid_argument_name(location: Range, name: &str) -> Option<Check> {
if !is_lower(name) {
pub fn invalid_argument_name(name: &str, location: Range) -> Option<Check> {
if name.to_lowercase() != name {
return Some(Check::new(
CheckKind::InvalidArgumentName(name.to_string()),
location,
@@ -53,21 +54,15 @@ pub fn invalid_argument_name(location: Range, name: &str) -> Option<Check> {
/// N804
pub fn invalid_first_argument_name_for_class_method(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
args: &Arguments,
settings: &Settings,
) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class) {
return None;
}
if decorator_list.iter().any(|decorator| {
if let ExprKind::Name { id, .. } = &decorator.node {
settings.classmethod_decorators.contains(id)
} else {
false
}
}) {
if matches!(
helpers::function_type(scope, name, decorator_list, settings),
FunctionType::ClassMethod
) {
if let Some(arg) = args.args.first() {
if arg.node.arg != "cls" {
return Some(Check::new(
@@ -83,31 +78,22 @@ pub fn invalid_first_argument_name_for_class_method(
/// N805
pub fn invalid_first_argument_name_for_method(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
args: &Arguments,
settings: &Settings,
) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class) {
return None;
}
if decorator_list.iter().any(|decorator| {
if let ExprKind::Name { id, .. } = &decorator.node {
settings.classmethod_decorators.contains(id)
|| settings.staticmethod_decorators.contains(id)
} else {
false
}
}) {
return None;
}
if let Some(arg) = args.args.first() {
if arg.node.arg != "self" {
return Some(Check::new(
CheckKind::InvalidFirstArgumentNameForMethod,
Range::from_located(arg),
));
if matches!(
helpers::function_type(scope, name, decorator_list, settings),
FunctionType::Method
) {
if let Some(arg) = args.args.first() {
if arg.node.arg != "self" {
return Some(Check::new(
CheckKind::InvalidFirstArgumentNameForMethod,
Range::from_located(arg),
));
}
}
}
None
@@ -128,18 +114,16 @@ pub fn non_lowercase_variable_in_function(scope: &Scope, expr: &Expr, name: &str
}
/// N807
pub fn dunder_function_name(func_def: &Stmt, scope: &Scope, name: &str) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class) {
pub fn dunder_function_name(scope: &Scope, stmt: &Stmt, name: &str) -> Option<Check> {
if matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
if name.starts_with("__") && name.ends_with("__") {
return Some(Check::new(
CheckKind::DunderFunctionName,
Range::from_located(func_def),
Range::from_located(stmt),
));
}
None
}
@@ -149,7 +133,7 @@ pub fn constant_imported_as_non_constant(
name: &str,
asname: &str,
) -> Option<Check> {
if is_upper(name) && !is_upper(asname) {
if helpers::is_upper(name) && !helpers::is_upper(asname) {
return Some(Check::new(
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -164,7 +148,7 @@ pub fn lowercase_imported_as_non_lowercase(
name: &str,
asname: &str,
) -> Option<Check> {
if is_lower(name) && asname.to_lowercase() != asname {
if !helpers::is_upper(name) && helpers::is_lower(name) && asname.to_lowercase() != asname {
return Some(Check::new(
CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -179,7 +163,7 @@ pub fn camelcase_imported_as_lowercase(
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_lower(asname) {
if helpers::is_camelcase(name) && helpers::is_lower(asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -194,7 +178,11 @@ pub fn camelcase_imported_as_constant(
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_upper(asname) && !is_acronym(name, asname) {
if helpers::is_camelcase(name)
&& !helpers::is_lower(asname)
&& helpers::is_upper(asname)
&& !helpers::is_acronym(name, asname)
{
return Some(Check::new(
CheckKind::CamelcaseImportedAsConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -205,10 +193,10 @@ pub fn camelcase_imported_as_constant(
/// N815
pub fn mixed_case_variable_in_class_scope(scope: &Scope, expr: &Expr, name: &str) -> Option<Check> {
if !matches!(scope.kind, ScopeKind::Class) {
if !matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
if is_mixed_case(name) {
if helpers::is_mixed_case(name) {
return Some(Check::new(
CheckKind::MixedCaseVariableInClassScope(name.to_string()),
Range::from_located(expr),
@@ -226,7 +214,7 @@ pub fn mixed_case_variable_in_global_scope(
if !matches!(scope.kind, ScopeKind::Module) {
return None;
}
if is_mixed_case(name) {
if helpers::is_mixed_case(name) {
return Some(Check::new(
CheckKind::MixedCaseVariableInGlobalScope(name.to_string()),
Range::from_located(expr),
@@ -241,7 +229,11 @@ pub fn camelcase_imported_as_acronym(
name: &str,
asname: &str,
) -> Option<Check> {
if is_camelcase(name) && is_upper(asname) && is_acronym(name, asname) {
if helpers::is_camelcase(name)
&& !helpers::is_lower(asname)
&& helpers::is_upper(asname)
&& helpers::is_acronym(name, asname)
{
return Some(Check::new(
CheckKind::CamelcaseImportedAsAcronym(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -272,101 +264,3 @@ pub fn error_suffix_on_exception_name(
}
None
}
fn is_lower(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_uppercase() {
return false;
} else if !cased && c.is_lowercase() {
cased = true;
}
}
cased
}
fn is_upper(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_lowercase() {
return false;
} else if (!cased) && c.is_uppercase() {
cased = true;
}
}
cased
}
fn is_camelcase(name: &str) -> bool {
!is_lower(name) && !is_upper(name) && !name.contains('_')
}
fn is_mixed_case(name: &str) -> bool {
!is_lower(name)
&& name
.strip_prefix('_')
.unwrap_or(name)
.chars()
.next()
.map_or_else(|| false, |c| c.is_lowercase())
}
fn is_acronym(name: &str, asname: &str) -> bool {
name.chars().filter(|c| c.is_uppercase()).join("") == asname
}
#[cfg(test)]
mod tests {
use super::{is_acronym, is_camelcase, is_lower, is_mixed_case, is_upper};
#[test]
fn test_is_lower() -> () {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
assert!(!is_lower("aBc"));
assert!(!is_lower("ABC"));
assert!(!is_lower(""));
assert!(!is_lower("_"));
}
#[test]
fn test_is_upper() -> () {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));
assert!(!is_upper("aBc"));
assert!(!is_upper("abc"));
assert!(!is_upper(""));
assert!(!is_upper("_"));
}
#[test]
fn test_is_camelcase() -> () {
assert!(is_camelcase("Camel"));
assert!(is_camelcase("CamelCase"));
assert!(!is_camelcase("camel"));
assert!(!is_camelcase("camel_case"));
assert!(!is_camelcase("CAMEL"));
assert!(!is_camelcase("CAMEL_CASE"));
}
#[test]
fn test_is_mixed_case() -> () {
assert!(is_mixed_case("mixedCase"));
assert!(is_mixed_case("mixed_Case"));
assert!(is_mixed_case("_mixed_Case"));
assert!(!is_mixed_case("mixed_case"));
assert!(!is_mixed_case("MIXED_CASE"));
assert!(!is_mixed_case(""));
assert!(!is_mixed_case("_"));
}
#[test]
fn test_is_acronym() -> () {
assert!(is_acronym("AB", "AB"));
assert!(is_acronym("AbcDef", "AD"));
assert!(!is_acronym("AbcDef", "Ad"));
assert!(!is_acronym("AbcDef", "AB"));
}
}

160
src/pep8_naming/helpers.rs Normal file
View File

@@ -0,0 +1,160 @@
use itertools::Itertools;
use rustpython_ast::{Expr, ExprKind};
use crate::ast::helpers::match_name_or_attr;
use crate::ast::types::{Scope, ScopeKind};
use crate::pep8_naming::settings::Settings;
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [&str; 2] = ["type", "ABCMeta"];
pub enum FunctionType {
Function,
Method,
ClassMethod,
StaticMethod,
}
/// Classify a function based on its scope, name, and decorators.
pub fn function_type(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
settings: &Settings,
) -> FunctionType {
if let ScopeKind::Class(scope) = &scope.kind {
// Special-case class method, like `__new__`.
if CLASS_METHODS.contains(&name)
// The class itself extends a known metaclass, so all methods are class methods.
|| scope.bases.iter().any(|expr| {
METACLASS_BASES
.iter()
.any(|target| match_name_or_attr(expr, target))
})
// The method is decorated with a class method decorator (like `@classmethod`).
|| decorator_list.iter().any(|expr| {
if let ExprKind::Name { id, .. } = &expr.node {
settings.classmethod_decorators.contains(id)
} else {
false
}
}) {
FunctionType::ClassMethod
} else if decorator_list.iter().any(|expr| {
if let ExprKind::Name { id, .. } = &expr.node {
settings.staticmethod_decorators.contains(id)
} else {
false
}
}) {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
FunctionType::StaticMethod
} else {
// It's an instance method.
FunctionType::Method
}
} else {
FunctionType::Function
}
}
pub fn is_lower(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_uppercase() {
return false;
} else if !cased && c.is_lowercase() {
cased = true;
}
}
cased
}
pub fn is_upper(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_lowercase() {
return false;
} else if !cased && c.is_uppercase() {
cased = true;
}
}
cased
}
pub fn is_camelcase(name: &str) -> bool {
!is_lower(name) && !is_upper(name) && !name.contains('_')
}
pub fn is_mixed_case(name: &str) -> bool {
!is_lower(name)
&& name
.strip_prefix('_')
.unwrap_or(name)
.chars()
.next()
.map_or_else(|| false, |c| c.is_lowercase())
}
pub fn is_acronym(name: &str, asname: &str) -> bool {
name.chars().filter(|c| c.is_uppercase()).join("") == asname
}
#[cfg(test)]
mod tests {
use crate::pep8_naming::helpers::{
is_acronym, is_camelcase, is_lower, is_mixed_case, is_upper,
};
#[test]
fn test_is_lower() -> () {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
assert!(!is_lower("aBc"));
assert!(!is_lower("ABC"));
assert!(!is_lower(""));
assert!(!is_lower("_"));
}
#[test]
fn test_is_upper() -> () {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));
assert!(!is_upper("aBc"));
assert!(!is_upper("abc"));
assert!(!is_upper(""));
assert!(!is_upper("_"));
}
#[test]
fn test_is_camelcase() -> () {
assert!(is_camelcase("Camel"));
assert!(is_camelcase("CamelCase"));
assert!(!is_camelcase("camel"));
assert!(!is_camelcase("camel_case"));
assert!(!is_camelcase("CAMEL"));
assert!(!is_camelcase("CAMEL_CASE"));
}
#[test]
fn test_is_mixed_case() -> () {
assert!(is_mixed_case("mixedCase"));
assert!(is_mixed_case("mixed_Case"));
assert!(is_mixed_case("_mixed_Case"));
assert!(!is_mixed_case("mixed_case"));
assert!(!is_mixed_case("MIXED_CASE"));
assert!(!is_mixed_case(""));
assert!(!is_mixed_case("_"));
}
#[test]
fn test_is_acronym() -> () {
assert!(is_acronym("AB", "AB"));
assert!(is_acronym("AbcDef", "AD"));
assert!(!is_acronym("AbcDef", "Ad"));
assert!(!is_acronym("AbcDef", "AB"));
}
}

View File

@@ -1,2 +1,3 @@
pub mod checks;
mod helpers;
pub mod settings;

View File

@@ -1,10 +1,8 @@
use std::collections::BTreeSet;
use itertools::izip;
use regex::Regex;
use rustpython_parser::ast::{
Arg, Arguments, Cmpop, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt,
StmtKind,
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
};
use crate::ast::types::{BindingKind, CheckLocator, FunctionScope, Range, Scope, ScopeKind};
@@ -166,45 +164,6 @@ pub fn repeated_keys(
checks
}
fn is_constant(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Constant { .. } => true,
ExprKind::Tuple { elts, .. } => elts.iter().all(is_constant),
_ => false,
}
}
fn is_singleton(expr: &Expr) -> bool {
matches!(
expr.node,
ExprKind::Constant {
value: Constant::None | Constant::Bool(_) | Constant::Ellipsis,
..
}
)
}
fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// F632
pub fn is_literal(left: &Expr, ops: &[Cmpop], comparators: &[Expr], location: Range) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
let mut left = left;
for (op, right) in izip!(ops, comparators) {
if matches!(op, Cmpop::Is | Cmpop::IsNot)
&& (is_constant_non_singleton(left) || is_constant_non_singleton(right))
{
checks.push(Check::new(CheckKind::IsLiteral, location));
}
left = right;
}
checks
}
/// F621, F622
pub fn starred_expressions(
elts: &[Expr],

View File

@@ -1,11 +1,14 @@
use anyhow::Result;
use libcst_native::{Codegen, ImportNames, NameOrAttribute, SmallStatement, Statement};
use libcst_native::{
Codegen, CompOp, Comparison, ComparisonTarget, Expr, Expression, ImportNames, NameOrAttribute,
SmallStatement, Statement,
};
use rustpython_ast::Stmt;
use crate::ast::types::Range;
use crate::autofix::{helpers, Fix};
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
use crate::cst::matchers::{match_expr, match_module};
use crate::source_code_locator::SourceCodeLocator;
/// Generate a Fix to remove any unused imports from an `import` statement.
@@ -138,3 +141,66 @@ pub fn remove_unused_import_froms(
))
}
}
fn match_comparison<'a, 'b>(expr: &'a mut Expr<'b>) -> Result<&'a mut Comparison<'b>> {
if let Expression::Comparison(comparison) = &mut expr.value {
Ok(comparison)
} else {
Err(anyhow::anyhow!(
"Expected node to be: Expression::Comparison"
))
}
}
/// Generate a Fix to replace invalid is/is not comparisons with equal/not equal
pub fn fix_invalid_literal_comparison(locator: &SourceCodeLocator, location: Range) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&location);
let mut tree = match_module(&module_text)?;
let mut expr = match_expr(&mut tree)?;
let cmp = match_comparison(expr)?;
let target = cmp
.comparisons
.get(0)
.ok_or_else(|| anyhow::anyhow!("Expected one ComparisonTarget"))?;
let new_operator = match &target.operator {
CompOp::Is {
whitespace_before: b,
whitespace_after: a,
} => Ok(CompOp::Equal {
whitespace_before: b.clone(),
whitespace_after: a.clone(),
}),
CompOp::IsNot {
whitespace_before: b,
whitespace_after: a,
whitespace_between: _,
} => Ok(CompOp::NotEqual {
whitespace_before: b.clone(),
whitespace_after: a.clone(),
}),
op => Err(anyhow::anyhow!(
"Unexpected operator: {:?}. Expected Is or IsNot.",
op
)),
}?;
expr.value = Expression::Comparison(Box::new(Comparison {
left: cmp.left.clone(),
comparisons: vec![ComparisonTarget {
operator: new_operator,
comparator: target.comparator.clone(),
}],
lpar: cmp.lpar.clone(),
rpar: cmp.rpar.clone(),
}));
let mut state = Default::default();
tree.codegen(&mut state);
Ok(Fix::replacement(
state.to_string(),
location.location,
location.end_location,
))
}

View File

@@ -0,0 +1,62 @@
use itertools::izip;
use log::error;
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::pyflakes::fixes::fix_invalid_literal_comparison;
fn is_singleton(expr: &Expr) -> bool {
matches!(
expr.node,
ExprKind::Constant {
value: Constant::None | Constant::Bool(_) | Constant::Ellipsis,
..
}
)
}
fn is_constant(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Constant { .. } => true,
ExprKind::Tuple { elts, .. } => elts.iter().all(is_constant),
_ => false,
}
}
fn is_constant_non_singleton(expr: &Expr) -> bool {
is_constant(expr) && !is_singleton(expr)
}
/// F632
pub fn invalid_literal_comparison(
checker: &mut Checker,
left: &Expr,
ops: &[Cmpop],
comparators: &[Expr],
location: Range,
) {
let mut left = left;
for (op, right) in izip!(ops, comparators) {
if matches!(op, Cmpop::Is | Cmpop::IsNot)
&& (is_constant_non_singleton(left) || is_constant_non_singleton(right))
{
let mut check = Check::new(CheckKind::IsLiteral, location);
if checker.patch() {
match fix_invalid_literal_comparison(
checker.locator,
Range {
location: left.location,
end_location: right.end_location.unwrap(),
},
) {
Ok(fix) => check.amend(fix),
Err(e) => error!("Failed to fix invalid comparison: {}", e),
}
}
checker.add_check(check);
}
left = right;
}
}

View File

@@ -1,9 +1,11 @@
pub use assert_tuple::assert_tuple;
pub use if_tuple::if_tuple;
pub use invalid_literal_comparisons::invalid_literal_comparison;
pub use invalid_print_syntax::invalid_print_syntax;
pub use raise_not_implemented::raise_not_implemented;
mod assert_tuple;
mod if_tuple;
mod invalid_literal_comparisons;
mod invalid_print_syntax;
mod raise_not_implemented;

View File

@@ -9,7 +9,7 @@ use crate::checks::CheckKind;
use crate::source_code_locator::SourceCodeLocator;
use crate::Check;
/// See: https://github.com/microsoft/vscode/blob/095ddabc52b82498ee7f718a34f9dd11d59099a8/src/vs/base/common/strings.ts#L1195
/// See: https://github.com/microsoft/vscode/blob/095ddabc52b82498ee7f718a34f9dd11d59099a8/src/vs/base/common/strings.ts#L1094
static CONFUSABLES: Lazy<BTreeMap<u32, u32>> = Lazy::new(|| {
BTreeMap::from([
(8232, 32),

View File

@@ -9,7 +9,16 @@ expression: checks
end_location:
row: 1
column: 13
fix: ~
fix:
patch:
content: "x == \"abc\""
location:
row: 1
column: 3
end_location:
row: 1
column: 13
applied: false
- kind: IsLiteral
location:
row: 4
@@ -17,5 +26,31 @@ expression: checks
end_location:
row: 4
column: 15
fix: ~
fix:
patch:
content: 123 != y
location:
row: 4
column: 3
end_location:
row: 4
column: 15
applied: false
- kind: IsLiteral
location:
row: 7
column: 9
end_location:
row: 7
column: 17
fix:
patch:
content: "\"123\" == x"
location:
row: 7
column: 3
end_location:
row: 7
column: 13
applied: false

View File

@@ -6,18 +6,18 @@ expression: checks
InvalidArgumentName: A
location:
row: 1
column: 12
column: 15
end_location:
row: 1
column: 13
column: 16
fix: ~
- kind:
InvalidArgumentName: A
location:
row: 6
column: 24
column: 27
end_location:
row: 6
column: 25
column: 28
fix: ~

View File

@@ -4,10 +4,18 @@ expression: checks
---
- kind: InvalidFirstArgumentNameForClassMethod
location:
row: 3
column: 25
row: 30
column: 26
end_location:
row: 3
column: 29
row: 30
column: 30
fix: ~
- kind: InvalidFirstArgumentNameForClassMethod
location:
row: 35
column: 19
end_location:
row: 35
column: 23
fix: ~

View File

@@ -0,0 +1,22 @@
---
source: src/linter.rs
expression: checks
---
- kind: PEP3120UnnecessaryCodingComment
location:
row: 1
column: 0
end_location:
row: 1
column: 14
fix:
patch:
content: ""
location:
row: 1
column: 0
end_location:
row: 1
column: 14
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/linter.rs
expression: checks
---
- kind: PEP3120UnnecessaryCodingComment
location:
row: 2
column: 0
end_location:
row: 2
column: 24
fix:
patch:
content: ""
location:
row: 2
column: 0
end_location:
row: 2
column: 24
applied: false

View File

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

View File

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